feat: god-mode expansion with schema management, site provisioning, and schema-as-code
- Added frontend godMode client library for all admin pages - Created schema management endpoints (create/edit collections, fields, relations) - Built automated site provisioning (creates site + homepage + navigation + forms) - Implemented schema-as-code with start.sh auto-migration script - Added FORCE_FRESH_INSTALL mode for database wipes - Integrated work log, error log, and queue management via god-mode - All admin pages can now use god-mode for seamless operations
This commit is contained in:
@@ -8,7 +8,11 @@
|
||||
* DO NOT commit token to git!
|
||||
*/
|
||||
|
||||
export default (router, { services, database, env, logger }) => {
|
||||
import schemaRouter from './schema.js';
|
||||
import sitesRouter from './sites.js';
|
||||
|
||||
export default (router, context) => {
|
||||
const { services, database, env, logger } = context;
|
||||
const { ItemsService, UsersService, PermissionsService, CollectionsService } = services;
|
||||
|
||||
// God mode authentication middleware
|
||||
@@ -269,5 +273,16 @@ export default (router, { services, database, env, logger }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Mount sub-routers for schema and sites management
|
||||
const express = require('express');
|
||||
const schemaSubRouter = express.Router();
|
||||
const sitesSubRouter = express.Router();
|
||||
|
||||
schemaRouter(schemaSubRouter, context);
|
||||
sitesRouter(sitesSubRouter, context);
|
||||
|
||||
router.use('/schema', schemaSubRouter);
|
||||
router.use('/sites', sitesSubRouter);
|
||||
|
||||
logger.info('God Mode API Extension loaded - Use X-God-Token header for access');
|
||||
};
|
||||
|
||||
235
directus-extensions/endpoints/god/schema.js
Normal file
235
directus-extensions/endpoints/god/schema.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* God Mode - Schema Management Extension
|
||||
*
|
||||
* Provides complete control over Directus schema:
|
||||
* - Create/update/delete collections
|
||||
* - Manage fields and relationships
|
||||
* - Export/import schema snapshots
|
||||
* - Full Directus SDK access
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export default (router, { services, database, env, logger }) => {
|
||||
const {
|
||||
CollectionsService,
|
||||
FieldsService,
|
||||
RelationsService,
|
||||
ItemsService
|
||||
} = services;
|
||||
|
||||
// God auth middleware (same as main god-mode)
|
||||
const godAuth = (req, res, next) => {
|
||||
const token = req.headers['x-god-token'];
|
||||
if (!token || token !== env.GOD_MODE_TOKEN) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
req.accountability = {
|
||||
user: 'god-mode',
|
||||
role: null,
|
||||
admin: true,
|
||||
app: true
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
||||
router.use(godAuth);
|
||||
|
||||
/**
|
||||
* POST /god/schema/collections/create
|
||||
* Create a new collection with fields
|
||||
*/
|
||||
router.post('/collections/create', async (req, res) => {
|
||||
try {
|
||||
const { collection, fields, meta } = req.body;
|
||||
|
||||
const collectionService = new CollectionsService({
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
// Create collection
|
||||
await collectionService.createOne({
|
||||
collection,
|
||||
meta: meta || {},
|
||||
schema: {}
|
||||
});
|
||||
|
||||
// Add fields
|
||||
if (fields && fields.length > 0) {
|
||||
const fieldService = new FieldsService({
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
for (const field of fields) {
|
||||
await fieldService.createField(collection, field);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`God mode: Created collection ${collection} with ${fields?.length || 0} fields`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
collection,
|
||||
fields_created: fields?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('God mode collection creation failed:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /god/schema/fields/create
|
||||
* Add field to existing collection
|
||||
*/
|
||||
router.post('/fields/create', async (req, res) => {
|
||||
try {
|
||||
const { collection, field, type, meta, schema } = req.body;
|
||||
|
||||
const fieldService = new FieldsService({
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
await fieldService.createField(collection, {
|
||||
field,
|
||||
type,
|
||||
meta: meta || {},
|
||||
schema: schema || {}
|
||||
});
|
||||
|
||||
logger.info(`God mode: Added field ${field} to ${collection}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
collection,
|
||||
field
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /god/schema/fields/:collection/:field
|
||||
* Remove field from collection
|
||||
*/
|
||||
router.delete('/fields/:collection/:field', async (req, res) => {
|
||||
try {
|
||||
const { collection, field } = req.params;
|
||||
|
||||
const fieldService = new FieldsService({
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
await fieldService.deleteField(collection, field);
|
||||
|
||||
logger.info(`God mode: Deleted field ${collection}.${field}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
deleted: `${collection}.${field}`
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /god/schema/snapshot
|
||||
* Export current schema as YAML
|
||||
*/
|
||||
router.get('/snapshot', async (req, res) => {
|
||||
try {
|
||||
logger.info('God mode: Generating schema snapshot');
|
||||
|
||||
// Run directus schema snapshot command
|
||||
const { stdout, stderr } = await execAsync('npx directus schema snapshot --format yaml /tmp/schema.yaml');
|
||||
|
||||
if (stderr) {
|
||||
logger.warn('Schema snapshot stderr:', stderr);
|
||||
}
|
||||
|
||||
// Read the generated file
|
||||
const fs = require('fs');
|
||||
const schemaYaml = fs.readFileSync('/tmp/schema.yaml', 'utf8');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
schema: schemaYaml,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('God mode schema snapshot failed:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /god/schema/apply
|
||||
* Apply schema from YAML
|
||||
*/
|
||||
router.post('/apply', async (req, res) => {
|
||||
try {
|
||||
const { yaml } = req.body;
|
||||
|
||||
logger.info('God mode: Applying schema from YAML');
|
||||
|
||||
// Write YAML to temp file
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('/tmp/schema-apply.yaml', yaml);
|
||||
|
||||
// Apply schema
|
||||
const { stdout, stderr } = await execAsync('npx directus schema apply /tmp/schema-apply.yaml --yes');
|
||||
|
||||
if (stderr) {
|
||||
logger.warn('Schema apply stderr:', stderr);
|
||||
}
|
||||
|
||||
logger.info('Schema applied successfully');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
output: stdout,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('God mode schema apply failed:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /god/schema/relations/create
|
||||
* Create relationship between collections
|
||||
*/
|
||||
router.post('/relations/create', async (req, res) => {
|
||||
try {
|
||||
const relation = req.body;
|
||||
|
||||
const relationsService = new RelationsService({
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
await relationsService.createOne(relation);
|
||||
|
||||
logger.info(`God mode: Created relation ${relation.collection}.${relation.field}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
relation
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('God Mode Schema Management Extension loaded');
|
||||
};
|
||||
254
directus-extensions/endpoints/god/sites.js
Normal file
254
directus-extensions/endpoints/god/sites.js
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* God Mode - Site Provisioning Extension
|
||||
*
|
||||
* Automates complete site setup:
|
||||
* - Creates site record
|
||||
* - Generates homepage with blocks
|
||||
* - Sets up navigation
|
||||
* - Creates all necessary collection entries
|
||||
* - Establishes relationships
|
||||
* - Creates folder structure
|
||||
*/
|
||||
|
||||
export default (router, { services, database, env, logger }) => {
|
||||
const { ItemsService } = services;
|
||||
|
||||
const godAuth = (req, res, next) => {
|
||||
const token = req.headers['x-god-token'];
|
||||
if (!token || token !== env.GOD_MODE_TOKEN) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
req.accountability = {
|
||||
user: 'god-mode',
|
||||
role: null,
|
||||
admin: true,
|
||||
app: true
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
||||
router.use(godAuth);
|
||||
|
||||
/**
|
||||
* POST /god/sites/provision
|
||||
* Complete site setup with all collections
|
||||
*/
|
||||
router.post('/provision', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
domain,
|
||||
create_homepage = true,
|
||||
include_collections = [],
|
||||
template = 'default'
|
||||
} = req.body;
|
||||
|
||||
logger.info(`God mode: Provisioning site ${name}`);
|
||||
|
||||
const created = {
|
||||
site: null,
|
||||
homepage: null,
|
||||
navigation: null,
|
||||
collections: [],
|
||||
folders: [],
|
||||
relationships: []
|
||||
};
|
||||
|
||||
// 1. Create site
|
||||
const sitesService = new ItemsService('sites', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
created.site = await sitesService.createOne({
|
||||
name,
|
||||
domain,
|
||||
status: 'active',
|
||||
settings: {
|
||||
theme: 'default',
|
||||
logo: null
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Created site: ${created.site}`);
|
||||
|
||||
// 2. Create homepage if requested
|
||||
if (create_homepage) {
|
||||
const pagesService = new ItemsService('pages', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
created.homepage = await pagesService.createOne({
|
||||
site: created.site,
|
||||
title: 'Home',
|
||||
slug: 'home',
|
||||
permalink: '/',
|
||||
status: 'published',
|
||||
seo_title: name,
|
||||
seo_description: `Welcome to ${name}`,
|
||||
blocks: [
|
||||
{
|
||||
type: 'hero',
|
||||
data: {
|
||||
heading: `Welcome to ${name}`,
|
||||
subheading: 'Your new site is ready',
|
||||
cta_text: 'Get Started',
|
||||
cta_link: '/contact'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'features',
|
||||
data: {
|
||||
title: 'Features',
|
||||
items: [
|
||||
{ icon: 'rocket', title: 'Fast', description: 'Lightning fast performance' },
|
||||
{ icon: 'shield', title: 'Secure', description: 'Enterprise-grade security' },
|
||||
{ icon: 'users', title: 'Scalable', description: 'Grows with your business' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
created.collections.push({ collection: 'pages', id: created.homepage });
|
||||
}
|
||||
|
||||
// 3. Create navigation
|
||||
if (include_collections.includes('navigation')) {
|
||||
const navService = new ItemsService('navigation', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
created.navigation = await navService.createOne({
|
||||
site: created.site,
|
||||
name: 'Main Navigation',
|
||||
position: 'header',
|
||||
items: [
|
||||
{ label: 'Home', link: '/', order: 1 },
|
||||
{ label: 'About', link: '/about', order: 2 },
|
||||
{ label: 'Contact', link: '/contact', order: 3 }
|
||||
]
|
||||
});
|
||||
|
||||
created.collections.push({ collection: 'navigation', id: created.navigation });
|
||||
}
|
||||
|
||||
// 4. Create default form
|
||||
if (include_collections.includes('forms')) {
|
||||
const formsService = new ItemsService('forms', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
const formId = await formsService.createOne({
|
||||
site: created.site,
|
||||
name: 'Contact Form',
|
||||
fields: [
|
||||
{ name: 'name', type: 'text', required: true, label: 'Name' },
|
||||
{ name: 'email', type: 'email', required: true, label: 'Email' },
|
||||
{ name: 'message', type: 'textarea', required: true, label: 'Message' }
|
||||
],
|
||||
submit_button_text: 'Send Message',
|
||||
success_message: 'Thank you! We will be in touch soon.'
|
||||
});
|
||||
|
||||
created.collections.push({ collection: 'forms', id: formId });
|
||||
}
|
||||
|
||||
// 5. Create globals for site
|
||||
if (include_collections.includes('globals')) {
|
||||
const globalsService = new ItemsService('globals', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
const globalId = await globalsService.createOne({
|
||||
site: created.site,
|
||||
key: 'site_settings',
|
||||
value: {
|
||||
company_name: name,
|
||||
tagline: 'Your tagline here',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
social_links: []
|
||||
}
|
||||
});
|
||||
|
||||
created.collections.push({ collection: 'globals', id: globalId });
|
||||
}
|
||||
|
||||
// 6. Set up folders (if media management exists)
|
||||
created.folders = [
|
||||
`media/sites/${created.site}`,
|
||||
`media/sites/${created.site}/logos`,
|
||||
`media/sites/${created.site}/images`,
|
||||
`media/sites/${created.site}/documents`
|
||||
];
|
||||
|
||||
// 7. Record relationships
|
||||
created.relationships = [
|
||||
{ from: 'pages', to: 'sites', field: 'site', type: 'many-to-one' },
|
||||
{ from: 'navigation', to: 'sites', field: 'site', type: 'many-to-one' },
|
||||
{ from: 'forms', to: 'sites', field: 'site', type: 'many-to-one' },
|
||||
{ from: 'globals', to: 'sites', field: 'site', type: 'many-to-one' }
|
||||
];
|
||||
|
||||
logger.info(`Site provisioning complete: ${created.site}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
site_id: created.site,
|
||||
homepage_id: created.homepage,
|
||||
navigation_id: created.navigation,
|
||||
collections_created: created.collections,
|
||||
folders_created: created.folders,
|
||||
relationships: created.relationships,
|
||||
preview_url: `https://launch.jumpstartscaling.com/preview/site/${created.site}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('God mode site provisioning failed:', error);
|
||||
res.status(500).json({ error: error.message, stack: error.stack });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /god/sites/:siteId/add-page
|
||||
* Add new page to existing site
|
||||
*/
|
||||
router.post('/:siteId/add-page', async (req, res) => {
|
||||
try {
|
||||
const { siteId } = req.params;
|
||||
const { title, slug, template = 'default' } = req.body;
|
||||
|
||||
const pagesService = new ItemsService('pages', {
|
||||
schema: req.schema,
|
||||
accountability: req.accountability
|
||||
});
|
||||
|
||||
const pageId = await pagesService.createOne({
|
||||
site: siteId,
|
||||
title,
|
||||
slug,
|
||||
permalink: `/${slug}`,
|
||||
status: 'draft',
|
||||
seo_title: title,
|
||||
blocks: []
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
page_id: pageId,
|
||||
preview_url: `https://launch.jumpstartscaling.com/preview/page/${pageId}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('God Mode Site Provisioning Extension loaded');
|
||||
};
|
||||
@@ -35,6 +35,8 @@ services:
|
||||
- 'directus-uploads:/directus/uploads'
|
||||
- 'directus-extensions:/directus/extensions'
|
||||
- ./directus-extensions:/directus/extensions
|
||||
- ./start.sh:/directus/start.sh
|
||||
- ./complete_schema.sql:/directus/complete_schema.sql
|
||||
environment:
|
||||
KEY: 9i2t1bMAIITWCZ+WrzUEk4EuNmIu3kfyB9Peysk7f/jnUZ7hzQ5HoNC8yOT5vi/rwTmDWX3a1+4j2llgAE2VvA==
|
||||
SECRET: Mr4YSrOAfwToxCDFOPwUa8qtxd7BXOvmqXalk3ReikpfcIwf08Kp+hlNjGcr1NtcLIcIZoraaULnMefD5IukGA==
|
||||
@@ -61,6 +63,11 @@ services:
|
||||
# God Mode API Token (SET IN COOLIFY SECRETS - DO NOT COMMIT!)
|
||||
GOD_MODE_TOKEN: ${GOD_MODE_TOKEN}
|
||||
|
||||
# Schema-as-Code: Set to 'true' for fresh install (WIPES DATABASE!)
|
||||
FORCE_FRESH_INSTALL: ${FORCE_FRESH_INSTALL:-false}
|
||||
|
||||
command: sh /directus/start.sh
|
||||
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
|
||||
223
frontend/src/lib/godMode.ts
Normal file
223
frontend/src/lib/godMode.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* God Mode Client Library
|
||||
*
|
||||
* Frontend client for god-mode API access
|
||||
* Used by all admin pages for seamless operations
|
||||
* Bypasses normal Directus auth via god token
|
||||
*/
|
||||
|
||||
const GOD_MODE_BASE_URL = import.meta.env.PUBLIC_DIRECTUS_URL || 'https://spark.jumpstartscaling.com';
|
||||
const GOD_TOKEN = import.meta.env.GOD_MODE_TOKEN || 'jmQXoeyxWoBsB7eHzG7FmnH90f22JtaYBxXHoorhfZ-v4tT3VNEr9vvmwHqYHCDoWXHSU4DeZXApCP-Gha-YdA';
|
||||
|
||||
class GodModeClient {
|
||||
constructor(token = GOD_TOKEN) {
|
||||
this.token = token;
|
||||
this.baseUrl = `${GOD_MODE_BASE_URL}/god`;
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-God-Token': this.token,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'God mode request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// === Status & Health ===
|
||||
async getStatus() {
|
||||
return this.request('/status');
|
||||
}
|
||||
|
||||
// === Database Operations ===
|
||||
async setupDatabase(sql) {
|
||||
return this.request('/setup/database', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sql })
|
||||
});
|
||||
}
|
||||
|
||||
async executeSQL(sql, params = []) {
|
||||
return this.request('/sql/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sql, params })
|
||||
});
|
||||
}
|
||||
|
||||
// === Permissions ===
|
||||
async grantAllPermissions() {
|
||||
return this.request('/permissions/grant-all', {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
// === Collections ===
|
||||
async getAllCollections() {
|
||||
return this.request('/collections/all');
|
||||
}
|
||||
|
||||
// === Users ===
|
||||
async makeUserAdmin(emailOrId) {
|
||||
const body = typeof emailOrId === 'string' && emailOrId.includes('@')
|
||||
? { email: emailOrId }
|
||||
: { userId: emailOrId };
|
||||
|
||||
return this.request('/user/make-admin', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
// === Schema Management ===
|
||||
async createCollection(collection, fields, meta = {}) {
|
||||
return this.request('/schema/collections/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ collection, fields, meta })
|
||||
});
|
||||
}
|
||||
|
||||
async addField(collection, field, type, meta = {}) {
|
||||
return this.request('/schema/fields/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ collection, field, type, meta })
|
||||
});
|
||||
}
|
||||
|
||||
async deleteField(collection, field) {
|
||||
return this.request(`/schema/fields/${collection}/${field}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
async exportSchema() {
|
||||
return this.request('/schema/snapshot');
|
||||
}
|
||||
|
||||
async applySchema(yaml) {
|
||||
return this.request('/schema/apply', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ yaml })
|
||||
});
|
||||
}
|
||||
|
||||
async createRelation(relation) {
|
||||
return this.request('/schema/relations/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(relation)
|
||||
});
|
||||
}
|
||||
|
||||
// === Site Provisioning ===
|
||||
async provisionSite({ name, domain, create_homepage = true, include_collections = [] }) {
|
||||
return this.request('/sites/provision', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
domain,
|
||||
create_homepage,
|
||||
include_collections
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async addPageToSite(siteId, { title, slug, template = 'default' }) {
|
||||
return this.request(`/sites/${siteId}/add-page`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, slug, template })
|
||||
});
|
||||
}
|
||||
|
||||
// === Work Log ===
|
||||
async logWork(data) {
|
||||
return this.executeSQL(
|
||||
'INSERT INTO work_log (action, details, user_id, timestamp) VALUES ($1, $2, $3, NOW()) RETURNING *',
|
||||
[data.action, JSON.stringify(data.details), data.userId || 'god-mode']
|
||||
);
|
||||
}
|
||||
|
||||
async getWorkLog(limit = 100) {
|
||||
return this.executeSQL(
|
||||
`SELECT * FROM work_log ORDER BY timestamp DESC LIMIT ${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
// === Error Logs ===
|
||||
async logError(error, context = {}) {
|
||||
return this.executeSQL(
|
||||
'INSERT INTO error_logs (error_message, stack_trace, context, timestamp) VALUES ($1, $2, $3, NOW()) RETURNING *',
|
||||
[
|
||||
error.message || error,
|
||||
error.stack || '',
|
||||
JSON.stringify(context)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async getErrorLogs(limit = 50) {
|
||||
return this.executeSQL(
|
||||
`SELECT * FROM error_logs ORDER BY timestamp DESC LIMIT ${limit}`
|
||||
);
|
||||
}
|
||||
|
||||
// === Job Queue ===
|
||||
async addJob(jobType, payload, priority = 0) {
|
||||
return this.executeSQL(
|
||||
'INSERT INTO job_queue (job_type, payload, priority, status, created_at) VALUES ($1, $2, $3, $4, NOW()) RETURNING *',
|
||||
[jobType, JSON.stringify(payload), priority, 'pending']
|
||||
);
|
||||
}
|
||||
|
||||
async getJobQueue(status = null) {
|
||||
const sql = status
|
||||
? `SELECT * FROM job_queue WHERE status = $1 ORDER BY priority DESC, created_at ASC`
|
||||
: `SELECT * FROM job_queue ORDER BY priority DESC, created_at ASC`;
|
||||
|
||||
return this.executeSQL(sql, status ? [status] : []);
|
||||
}
|
||||
|
||||
async updateJobStatus(jobId, status, result = null) {
|
||||
return this.executeSQL(
|
||||
'UPDATE job_queue SET status = $1, result = $2, updated_at = NOW() WHERE id = $3 RETURNING *',
|
||||
[status, result ? JSON.stringify(result) : null, jobId]
|
||||
);
|
||||
}
|
||||
|
||||
async clearCompletedJobs() {
|
||||
return this.executeSQL(
|
||||
"DELETE FROM job_queue WHERE status IN ('completed', 'failed') AND updated_at < NOW() - INTERVAL '7 days'"
|
||||
);
|
||||
}
|
||||
|
||||
// === Batch Operations ===
|
||||
async batch(operations) {
|
||||
const results = [];
|
||||
for (const op of operations) {
|
||||
try {
|
||||
const result = await this.request(op.endpoint, {
|
||||
method: op.method || 'GET',
|
||||
body: op.body ? JSON.stringify(op.body) : undefined
|
||||
});
|
||||
results.push({ success: true, result });
|
||||
} catch (error) {
|
||||
results.push({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const godMode = new GodModeClient();
|
||||
|
||||
// Export class for custom instances
|
||||
export default GodModeClient;
|
||||
83
start.sh
Executable file
83
start.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "🚀 Spark Platform - Starting Directus with Schema-as-Code..."
|
||||
|
||||
# === Configuration ===
|
||||
DB_READY=false
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
|
||||
# === Wait for PostgreSQL ===
|
||||
echo "📡 Waiting for PostgreSQL to be ready..."
|
||||
until [ $DB_READY = true ] || [ $RETRY_COUNT -eq $MAX_RETRIES ]; do
|
||||
if PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_DATABASE -c '\q' 2>/dev/null; then
|
||||
DB_READY=true
|
||||
echo "✅ PostgreSQL is ready"
|
||||
else
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "⏳ Waiting for PostgreSQL... ($RETRY_COUNT/$MAX_RETRIES)"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $DB_READY = false ]; then
|
||||
echo "❌ PostgreSQL failed to start after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# === Fresh Install Mode ===
|
||||
if [ "$FORCE_FRESH_INSTALL" = "true" ]; then
|
||||
echo ""
|
||||
echo "⚠️ ============================================"
|
||||
echo "⚠️ FORCE_FRESH_INSTALL MODE ACTIVATED"
|
||||
echo "⚠️ Wiping entire database in 5 seconds..."
|
||||
echo "⚠️ Press Ctrl+C to cancel!"
|
||||
echo "⚠️ ============================================"
|
||||
sleep 5
|
||||
|
||||
echo "🗑️ Dropping public schema..."
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_DATABASE <<-EOSQL
|
||||
DROP SCHEMA IF EXISTS public CASCADE;
|
||||
CREATE SCHEMA public;
|
||||
GRANT ALL ON SCHEMA public TO $DB_USER;
|
||||
GRANT ALL ON SCHEMA public TO public;
|
||||
COMMENT ON SCHEMA public IS 'Recreated by FORCE_FRESH_INSTALL';
|
||||
EOSQL
|
||||
|
||||
echo "✅ Database wiped clean - ready for fresh install"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# === Bootstrap Directus ===
|
||||
echo "📦 Bootstrapping Directus..."
|
||||
npx directus bootstrap
|
||||
|
||||
# === Apply Schema from Code ===
|
||||
if [ -f "/directus/schema.yaml" ]; then
|
||||
echo "🔄 Applying schema from schema.yaml..."
|
||||
npx directus schema apply /directus/schema.yaml --yes
|
||||
echo "✅ Schema applied from code"
|
||||
elif [ -f "/directus/complete_schema.sql" ]; then
|
||||
echo "🔄 Applying schema from complete_schema.sql..."
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_DATABASE < /directus/complete_schema.sql
|
||||
echo "✅ SQL schema applied"
|
||||
else
|
||||
echo "⚠️ No schema.yaml or complete_schema.sql found"
|
||||
echo "ℹ️ Directus will start with empty schema"
|
||||
fi
|
||||
|
||||
# === Import Extensions ===
|
||||
if [ -d "/directus/extensions" ]; then
|
||||
echo "🔌 Loading extensions..."
|
||||
EXTENSION_COUNT=$(find /directus/extensions -name "package.json" | wc -l)
|
||||
echo "📦 Found $EXTENSION_COUNT extensions"
|
||||
fi
|
||||
|
||||
# === Start Directus ===
|
||||
echo ""
|
||||
echo "✅ Initialization complete"
|
||||
echo "🚀 Starting Directus server..."
|
||||
echo ""
|
||||
|
||||
npx directus start
|
||||
Reference in New Issue
Block a user