@compilr-dev/factory 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/factory/file-writer.d.ts +11 -0
- package/dist/factory/file-writer.js +21 -0
- package/dist/factory/list-toolkits-tool.d.ts +12 -0
- package/dist/factory/list-toolkits-tool.js +35 -0
- package/dist/factory/registry.d.ts +15 -0
- package/dist/factory/registry.js +28 -0
- package/dist/factory/scaffold-tool.d.ts +21 -0
- package/dist/factory/scaffold-tool.js +95 -0
- package/dist/factory/skill.d.ts +14 -0
- package/dist/factory/skill.js +146 -0
- package/dist/factory/tools.d.ts +14 -0
- package/dist/factory/tools.js +17 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +25 -0
- package/dist/model/defaults.d.ts +10 -0
- package/dist/model/defaults.js +68 -0
- package/dist/model/naming.d.ts +12 -0
- package/dist/model/naming.js +80 -0
- package/dist/model/operations-entity.d.ts +35 -0
- package/dist/model/operations-entity.js +110 -0
- package/dist/model/operations-field.d.ts +33 -0
- package/dist/model/operations-field.js +104 -0
- package/dist/model/operations-relationship.d.ts +19 -0
- package/dist/model/operations-relationship.js +90 -0
- package/dist/model/operations-section.d.ts +32 -0
- package/dist/model/operations-section.js +35 -0
- package/dist/model/operations.d.ts +12 -0
- package/dist/model/operations.js +63 -0
- package/dist/model/persistence.d.ts +19 -0
- package/dist/model/persistence.js +40 -0
- package/dist/model/schema.d.ts +15 -0
- package/dist/model/schema.js +269 -0
- package/dist/model/tools.d.ts +14 -0
- package/dist/model/tools.js +380 -0
- package/dist/model/types.d.ts +70 -0
- package/dist/model/types.js +8 -0
- package/dist/toolkits/react-node/api.d.ts +8 -0
- package/dist/toolkits/react-node/api.js +198 -0
- package/dist/toolkits/react-node/config.d.ts +9 -0
- package/dist/toolkits/react-node/config.js +120 -0
- package/dist/toolkits/react-node/dashboard.d.ts +8 -0
- package/dist/toolkits/react-node/dashboard.js +60 -0
- package/dist/toolkits/react-node/entity.d.ts +8 -0
- package/dist/toolkits/react-node/entity.js +469 -0
- package/dist/toolkits/react-node/helpers.d.ts +27 -0
- package/dist/toolkits/react-node/helpers.js +71 -0
- package/dist/toolkits/react-node/index.d.ts +8 -0
- package/dist/toolkits/react-node/index.js +52 -0
- package/dist/toolkits/react-node/router.d.ts +8 -0
- package/dist/toolkits/react-node/router.js +49 -0
- package/dist/toolkits/react-node/seed.d.ts +11 -0
- package/dist/toolkits/react-node/seed.js +144 -0
- package/dist/toolkits/react-node/shared.d.ts +7 -0
- package/dist/toolkits/react-node/shared.js +119 -0
- package/dist/toolkits/react-node/shell.d.ts +8 -0
- package/dist/toolkits/react-node/shell.js +152 -0
- package/dist/toolkits/react-node/static.d.ts +8 -0
- package/dist/toolkits/react-node/static.js +105 -0
- package/dist/toolkits/react-node/types-gen.d.ts +8 -0
- package/dist/toolkits/react-node/types-gen.js +31 -0
- package/dist/toolkits/types.d.ts +22 -0
- package/dist/toolkits/types.js +6 -0
- package/package.json +51 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Tools
|
|
3
|
+
*
|
|
4
|
+
* app_model_get — scoped reads of the Application Model
|
|
5
|
+
* app_model_update — semantic operations on the model
|
|
6
|
+
* app_model_validate — check model health
|
|
7
|
+
*/
|
|
8
|
+
import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
|
|
9
|
+
import { ModelStore } from './persistence.js';
|
|
10
|
+
import { applyOperation } from './operations.js';
|
|
11
|
+
import { validateModel } from './schema.js';
|
|
12
|
+
import { createDefaultModel } from './defaults.js';
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Helpers
|
|
15
|
+
// =============================================================================
|
|
16
|
+
function getProjectId(config, inputProjectId) {
|
|
17
|
+
const projectId = inputProjectId ?? config.context.currentProjectId;
|
|
18
|
+
if (!projectId) {
|
|
19
|
+
throw new Error('No active project. Use project_create or project_get first.');
|
|
20
|
+
}
|
|
21
|
+
return projectId;
|
|
22
|
+
}
|
|
23
|
+
function createStore(config, projectId) {
|
|
24
|
+
return new ModelStore({
|
|
25
|
+
documents: config.context.documents,
|
|
26
|
+
projectId,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createSummary(model) {
|
|
30
|
+
return {
|
|
31
|
+
name: model.identity.name,
|
|
32
|
+
description: model.identity.description,
|
|
33
|
+
entityCount: model.entities.length,
|
|
34
|
+
entities: model.entities.map((e) => ({
|
|
35
|
+
name: e.name,
|
|
36
|
+
fieldCount: e.fields.length,
|
|
37
|
+
views: e.views,
|
|
38
|
+
relationshipCount: e.relationships.length,
|
|
39
|
+
})),
|
|
40
|
+
features: model.features,
|
|
41
|
+
toolkit: model.techStack.toolkit,
|
|
42
|
+
revision: model.meta.revision,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createAppModelGetTool(config) {
|
|
46
|
+
return defineTool({
|
|
47
|
+
name: 'app_model_get',
|
|
48
|
+
description: 'Read the Application Model for the current project. Supports scoped reads: no params = full model, scope="summary" for overview, scope="identity"/"features"/"layout"/"theme"/"techStack" for sections, entity="Name" for a single entity.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
scope: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
enum: ['identity', 'summary', 'features', 'layout', 'theme', 'techStack'],
|
|
55
|
+
description: 'Which section to return. Omit for full model. Use "summary" for a lightweight overview.',
|
|
56
|
+
},
|
|
57
|
+
entity: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Return only this entity (by PascalCase name). Overrides scope.',
|
|
60
|
+
},
|
|
61
|
+
project_id: {
|
|
62
|
+
type: 'number',
|
|
63
|
+
description: 'Project ID. Uses active project if omitted.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: [],
|
|
67
|
+
},
|
|
68
|
+
execute: async (input) => {
|
|
69
|
+
try {
|
|
70
|
+
const projectId = getProjectId(config, input.project_id);
|
|
71
|
+
const store = createStore(config, projectId);
|
|
72
|
+
const model = await store.get();
|
|
73
|
+
if (!model) {
|
|
74
|
+
return createSuccessResult({
|
|
75
|
+
exists: false,
|
|
76
|
+
message: 'No Application Model found. Use app_model_update with op: "addEntity" to start building one, or use the factory-scaffold skill.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Entity-scoped read
|
|
80
|
+
if (input.entity) {
|
|
81
|
+
const entity = model.entities.find((e) => e.name === input.entity);
|
|
82
|
+
if (!entity) {
|
|
83
|
+
return createErrorResult(`Entity "${input.entity}" not found`);
|
|
84
|
+
}
|
|
85
|
+
return createSuccessResult({ entity, meta: model.meta });
|
|
86
|
+
}
|
|
87
|
+
// Section-scoped read
|
|
88
|
+
switch (input.scope) {
|
|
89
|
+
case 'identity':
|
|
90
|
+
return createSuccessResult({ identity: model.identity, meta: model.meta });
|
|
91
|
+
case 'summary':
|
|
92
|
+
return createSuccessResult(createSummary(model));
|
|
93
|
+
case 'features':
|
|
94
|
+
return createSuccessResult({ features: model.features, meta: model.meta });
|
|
95
|
+
case 'layout':
|
|
96
|
+
return createSuccessResult({ layout: model.layout, meta: model.meta });
|
|
97
|
+
case 'theme':
|
|
98
|
+
return createSuccessResult({ theme: model.theme, meta: model.meta });
|
|
99
|
+
case 'techStack':
|
|
100
|
+
return createSuccessResult({ techStack: model.techStack, meta: model.meta });
|
|
101
|
+
default:
|
|
102
|
+
return createSuccessResult(model);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
readonly: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function createAppModelUpdateTool(config) {
|
|
113
|
+
return defineTool({
|
|
114
|
+
name: 'app_model_update',
|
|
115
|
+
description: `Apply a semantic operation to the Application Model. Operations: addEntity, updateEntity, removeEntity, renameEntity, reorderEntities, addField, updateField, removeField, renameField, addRelationship, removeRelationship, updateIdentity, updateLayout, updateFeatures, updateTheme, updateTechStack. If no model exists, one is created automatically with the first addEntity.`,
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
op: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
enum: [
|
|
122
|
+
'addEntity',
|
|
123
|
+
'updateEntity',
|
|
124
|
+
'removeEntity',
|
|
125
|
+
'renameEntity',
|
|
126
|
+
'reorderEntities',
|
|
127
|
+
'addField',
|
|
128
|
+
'updateField',
|
|
129
|
+
'removeField',
|
|
130
|
+
'renameField',
|
|
131
|
+
'addRelationship',
|
|
132
|
+
'removeRelationship',
|
|
133
|
+
'updateIdentity',
|
|
134
|
+
'updateLayout',
|
|
135
|
+
'updateFeatures',
|
|
136
|
+
'updateTheme',
|
|
137
|
+
'updateTechStack',
|
|
138
|
+
],
|
|
139
|
+
description: 'The operation to perform.',
|
|
140
|
+
},
|
|
141
|
+
revision: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
description: 'Expected current revision for optimistic locking. Optional.',
|
|
144
|
+
},
|
|
145
|
+
entity: {
|
|
146
|
+
description: 'Entity name (string) or full entity object (for addEntity).',
|
|
147
|
+
},
|
|
148
|
+
field: {
|
|
149
|
+
description: 'Field name (string) or full field object (for addField).',
|
|
150
|
+
},
|
|
151
|
+
relationship: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
description: 'Relationship object for addRelationship.',
|
|
154
|
+
properties: {
|
|
155
|
+
type: { type: 'string', enum: ['belongsTo', 'hasMany'] },
|
|
156
|
+
target: { type: 'string' },
|
|
157
|
+
fieldName: { type: 'string' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
updates: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
description: 'Partial updates for update operations.',
|
|
163
|
+
},
|
|
164
|
+
newName: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'New name for renameEntity or renameField.',
|
|
167
|
+
},
|
|
168
|
+
order: {
|
|
169
|
+
type: 'array',
|
|
170
|
+
items: { type: 'string' },
|
|
171
|
+
description: 'Entity name order for reorderEntities.',
|
|
172
|
+
},
|
|
173
|
+
target: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Relationship target entity name for removeRelationship.',
|
|
176
|
+
},
|
|
177
|
+
force: {
|
|
178
|
+
type: 'boolean',
|
|
179
|
+
description: 'Force removal with cascading cleanup.',
|
|
180
|
+
},
|
|
181
|
+
project_id: {
|
|
182
|
+
type: 'number',
|
|
183
|
+
description: 'Project ID. Uses active project if omitted.',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ['op'],
|
|
187
|
+
},
|
|
188
|
+
execute: async (input) => {
|
|
189
|
+
try {
|
|
190
|
+
const projectId = getProjectId(config, input.project_id);
|
|
191
|
+
const store = createStore(config, projectId);
|
|
192
|
+
// Load current model or create default
|
|
193
|
+
let model = await store.get();
|
|
194
|
+
if (!model) {
|
|
195
|
+
model = createDefaultModel();
|
|
196
|
+
}
|
|
197
|
+
// Optimistic locking
|
|
198
|
+
if (input.revision !== undefined && input.revision !== model.meta.revision) {
|
|
199
|
+
return createErrorResult(`Revision conflict: expected ${String(input.revision)}, current is ${String(model.meta.revision)}. Re-read the model and retry.`);
|
|
200
|
+
}
|
|
201
|
+
// Build the operation from the flat input
|
|
202
|
+
const operation = buildOperation(input);
|
|
203
|
+
// Apply
|
|
204
|
+
const updated = applyOperation(model, operation);
|
|
205
|
+
// Validate result
|
|
206
|
+
const validation = validateModel(updated);
|
|
207
|
+
if (!validation.valid) {
|
|
208
|
+
const msgs = validation.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
209
|
+
return createErrorResult(`Operation would produce invalid model: ${msgs}`);
|
|
210
|
+
}
|
|
211
|
+
// Save
|
|
212
|
+
await store.save(updated);
|
|
213
|
+
return createSuccessResult({
|
|
214
|
+
op: input.op,
|
|
215
|
+
revision: updated.meta.revision,
|
|
216
|
+
message: `Operation "${input.op}" applied successfully.`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function buildOperation(input) {
|
|
226
|
+
switch (input.op) {
|
|
227
|
+
case 'addEntity': {
|
|
228
|
+
if (!input.entity || typeof input.entity !== 'object') {
|
|
229
|
+
throw new Error('addEntity requires entity object');
|
|
230
|
+
}
|
|
231
|
+
const entity = input.entity;
|
|
232
|
+
return { op: 'addEntity', entity };
|
|
233
|
+
}
|
|
234
|
+
case 'updateEntity':
|
|
235
|
+
if (typeof input.entity !== 'string')
|
|
236
|
+
throw new Error('updateEntity requires entity name (string)');
|
|
237
|
+
if (!input.updates)
|
|
238
|
+
throw new Error('updateEntity requires updates object');
|
|
239
|
+
return { op: 'updateEntity', entity: input.entity, updates: input.updates };
|
|
240
|
+
case 'removeEntity':
|
|
241
|
+
if (typeof input.entity !== 'string')
|
|
242
|
+
throw new Error('removeEntity requires entity name (string)');
|
|
243
|
+
return { op: 'removeEntity', entity: input.entity, force: input.force };
|
|
244
|
+
case 'renameEntity':
|
|
245
|
+
if (typeof input.entity !== 'string')
|
|
246
|
+
throw new Error('renameEntity requires entity name (string)');
|
|
247
|
+
if (!input.newName)
|
|
248
|
+
throw new Error('renameEntity requires newName');
|
|
249
|
+
return { op: 'renameEntity', entity: input.entity, newName: input.newName };
|
|
250
|
+
case 'reorderEntities':
|
|
251
|
+
if (!input.order)
|
|
252
|
+
throw new Error('reorderEntities requires order array');
|
|
253
|
+
return { op: 'reorderEntities', order: input.order };
|
|
254
|
+
case 'addField': {
|
|
255
|
+
if (typeof input.entity !== 'string')
|
|
256
|
+
throw new Error('addField requires entity name (string)');
|
|
257
|
+
if (!input.field || typeof input.field !== 'object')
|
|
258
|
+
throw new Error('addField requires field object');
|
|
259
|
+
const field = input.field;
|
|
260
|
+
return { op: 'addField', entity: input.entity, field };
|
|
261
|
+
}
|
|
262
|
+
case 'updateField':
|
|
263
|
+
if (typeof input.entity !== 'string')
|
|
264
|
+
throw new Error('updateField requires entity name (string)');
|
|
265
|
+
if (typeof input.field !== 'string')
|
|
266
|
+
throw new Error('updateField requires field name (string)');
|
|
267
|
+
if (!input.updates)
|
|
268
|
+
throw new Error('updateField requires updates object');
|
|
269
|
+
return {
|
|
270
|
+
op: 'updateField',
|
|
271
|
+
entity: input.entity,
|
|
272
|
+
field: input.field,
|
|
273
|
+
updates: input.updates,
|
|
274
|
+
};
|
|
275
|
+
case 'removeField':
|
|
276
|
+
if (typeof input.entity !== 'string')
|
|
277
|
+
throw new Error('removeField requires entity name (string)');
|
|
278
|
+
if (typeof input.field !== 'string')
|
|
279
|
+
throw new Error('removeField requires field name (string)');
|
|
280
|
+
return { op: 'removeField', entity: input.entity, field: input.field };
|
|
281
|
+
case 'renameField':
|
|
282
|
+
if (typeof input.entity !== 'string')
|
|
283
|
+
throw new Error('renameField requires entity name (string)');
|
|
284
|
+
if (typeof input.field !== 'string')
|
|
285
|
+
throw new Error('renameField requires field name (string)');
|
|
286
|
+
if (!input.newName)
|
|
287
|
+
throw new Error('renameField requires newName');
|
|
288
|
+
return {
|
|
289
|
+
op: 'renameField',
|
|
290
|
+
entity: input.entity,
|
|
291
|
+
field: input.field,
|
|
292
|
+
newName: input.newName,
|
|
293
|
+
};
|
|
294
|
+
case 'addRelationship': {
|
|
295
|
+
if (typeof input.entity !== 'string')
|
|
296
|
+
throw new Error('addRelationship requires entity name (string)');
|
|
297
|
+
if (!input.relationship)
|
|
298
|
+
throw new Error('addRelationship requires relationship object');
|
|
299
|
+
const relationship = input.relationship;
|
|
300
|
+
return { op: 'addRelationship', entity: input.entity, relationship };
|
|
301
|
+
}
|
|
302
|
+
case 'removeRelationship':
|
|
303
|
+
if (typeof input.entity !== 'string')
|
|
304
|
+
throw new Error('removeRelationship requires entity name (string)');
|
|
305
|
+
if (!input.target)
|
|
306
|
+
throw new Error('removeRelationship requires target');
|
|
307
|
+
return { op: 'removeRelationship', entity: input.entity, target: input.target };
|
|
308
|
+
case 'updateIdentity':
|
|
309
|
+
if (!input.updates)
|
|
310
|
+
throw new Error('updateIdentity requires updates object');
|
|
311
|
+
return { op: 'updateIdentity', updates: input.updates };
|
|
312
|
+
case 'updateLayout':
|
|
313
|
+
if (!input.updates)
|
|
314
|
+
throw new Error('updateLayout requires updates object');
|
|
315
|
+
return { op: 'updateLayout', updates: input.updates };
|
|
316
|
+
case 'updateFeatures':
|
|
317
|
+
if (!input.updates)
|
|
318
|
+
throw new Error('updateFeatures requires updates object');
|
|
319
|
+
return { op: 'updateFeatures', updates: input.updates };
|
|
320
|
+
case 'updateTheme':
|
|
321
|
+
if (!input.updates)
|
|
322
|
+
throw new Error('updateTheme requires updates object');
|
|
323
|
+
return { op: 'updateTheme', updates: input.updates };
|
|
324
|
+
case 'updateTechStack':
|
|
325
|
+
if (!input.updates)
|
|
326
|
+
throw new Error('updateTechStack requires updates object');
|
|
327
|
+
return { op: 'updateTechStack', updates: input.updates };
|
|
328
|
+
default:
|
|
329
|
+
throw new Error(`Unknown operation: ${input.op}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function createAppModelValidateTool(config) {
|
|
333
|
+
return defineTool({
|
|
334
|
+
name: 'app_model_validate',
|
|
335
|
+
description: 'Validate the Application Model for the current project. Returns validation errors or confirms the model is valid.',
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
project_id: {
|
|
340
|
+
type: 'number',
|
|
341
|
+
description: 'Project ID. Uses active project if omitted.',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
required: [],
|
|
345
|
+
},
|
|
346
|
+
execute: async (input) => {
|
|
347
|
+
try {
|
|
348
|
+
const projectId = getProjectId(config, input.project_id);
|
|
349
|
+
const store = createStore(config, projectId);
|
|
350
|
+
const model = await store.get();
|
|
351
|
+
if (!model) {
|
|
352
|
+
return createSuccessResult({
|
|
353
|
+
valid: false,
|
|
354
|
+
errors: [{ path: '', message: 'No Application Model found' }],
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const result = validateModel(model);
|
|
358
|
+
return createSuccessResult({
|
|
359
|
+
valid: result.valid,
|
|
360
|
+
errors: result.errors,
|
|
361
|
+
revision: model.meta.revision,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
readonly: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// =============================================================================
|
|
372
|
+
// Public API
|
|
373
|
+
// =============================================================================
|
|
374
|
+
export function createModelTools(config) {
|
|
375
|
+
return [
|
|
376
|
+
createAppModelGetTool(config),
|
|
377
|
+
createAppModelUpdateTool(config),
|
|
378
|
+
createAppModelValidateTool(config),
|
|
379
|
+
];
|
|
380
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Model Types
|
|
3
|
+
*
|
|
4
|
+
* The Application Model is the central data structure of the factory system.
|
|
5
|
+
* It describes WHAT an application is — entities, fields, relationships,
|
|
6
|
+
* layout, features, theme — independently of how it's built.
|
|
7
|
+
*/
|
|
8
|
+
export interface ApplicationModel {
|
|
9
|
+
readonly identity: Identity;
|
|
10
|
+
readonly entities: readonly Entity[];
|
|
11
|
+
readonly layout: Layout;
|
|
12
|
+
readonly features: Features;
|
|
13
|
+
readonly theme: Theme;
|
|
14
|
+
readonly techStack: TechStack;
|
|
15
|
+
readonly meta: Meta;
|
|
16
|
+
}
|
|
17
|
+
export interface Identity {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
readonly version: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Entity {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly pluralName: string;
|
|
25
|
+
readonly description?: string;
|
|
26
|
+
readonly icon: string;
|
|
27
|
+
readonly fields: readonly Field[];
|
|
28
|
+
readonly views: readonly View[];
|
|
29
|
+
readonly relationships: readonly Relationship[];
|
|
30
|
+
}
|
|
31
|
+
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'enum';
|
|
32
|
+
export interface Field {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly label: string;
|
|
35
|
+
readonly type: FieldType;
|
|
36
|
+
readonly required: boolean;
|
|
37
|
+
readonly enumValues?: readonly string[];
|
|
38
|
+
readonly defaultValue?: string | number | boolean;
|
|
39
|
+
}
|
|
40
|
+
export type View = 'card' | 'list' | 'detail';
|
|
41
|
+
export type RelationshipType = 'belongsTo' | 'hasMany';
|
|
42
|
+
export interface Relationship {
|
|
43
|
+
readonly type: RelationshipType;
|
|
44
|
+
readonly target: string;
|
|
45
|
+
readonly fieldName?: string;
|
|
46
|
+
}
|
|
47
|
+
export type ShellType = 'sidebar-header' | 'header-only';
|
|
48
|
+
export interface Layout {
|
|
49
|
+
readonly shell: ShellType;
|
|
50
|
+
}
|
|
51
|
+
export interface Features {
|
|
52
|
+
readonly dashboard: boolean;
|
|
53
|
+
readonly auth: boolean;
|
|
54
|
+
readonly userProfiles: boolean;
|
|
55
|
+
readonly settings: boolean;
|
|
56
|
+
readonly darkMode: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface Theme {
|
|
59
|
+
readonly primaryColor: string;
|
|
60
|
+
}
|
|
61
|
+
export interface TechStack {
|
|
62
|
+
readonly toolkit: string;
|
|
63
|
+
}
|
|
64
|
+
export interface Meta {
|
|
65
|
+
readonly revision: number;
|
|
66
|
+
readonly createdAt: string;
|
|
67
|
+
readonly updatedAt: string;
|
|
68
|
+
readonly factoryVersion: string;
|
|
69
|
+
readonly generatedAt?: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Model Types
|
|
3
|
+
*
|
|
4
|
+
* The Application Model is the central data structure of the factory system.
|
|
5
|
+
* It describes WHAT an application is — entities, fields, relationships,
|
|
6
|
+
* layout, features, theme — independently of how it's built.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/index.ts, server/routes/{entity}.ts, server/data/{entity}.ts
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateApiFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/index.ts, server/routes/{entity}.ts, server/data/{entity}.ts
|
|
5
|
+
*/
|
|
6
|
+
import { toCamelCase, toKebabCase } from '../../model/naming.js';
|
|
7
|
+
import { tsType, fkFieldName, belongsToRels } from './helpers.js';
|
|
8
|
+
import { generateSeedData } from './seed.js';
|
|
9
|
+
export function generateApiFiles(model) {
|
|
10
|
+
return [
|
|
11
|
+
generateServerIndex(model),
|
|
12
|
+
...model.entities.flatMap((entity) => [
|
|
13
|
+
generateDataStore(model, entity),
|
|
14
|
+
generateRoutes(model, entity),
|
|
15
|
+
]),
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Server Entry Point
|
|
20
|
+
// =============================================================================
|
|
21
|
+
function generateServerIndex(model) {
|
|
22
|
+
const routeImports = model.entities
|
|
23
|
+
.map((e) => {
|
|
24
|
+
const varName = toCamelCase(e.pluralName) + 'Router';
|
|
25
|
+
const fileName = toKebabCase(e.pluralName).toLowerCase();
|
|
26
|
+
return `import { ${varName} } from './routes/${fileName}.js';`;
|
|
27
|
+
})
|
|
28
|
+
.join('\n');
|
|
29
|
+
const routeMounts = model.entities
|
|
30
|
+
.map((e) => {
|
|
31
|
+
const varName = toCamelCase(e.pluralName) + 'Router';
|
|
32
|
+
const apiRoute = '/api/' + toKebabCase(e.pluralName).toLowerCase();
|
|
33
|
+
return `app.use('${apiRoute}', ${varName});`;
|
|
34
|
+
})
|
|
35
|
+
.join('\n');
|
|
36
|
+
return {
|
|
37
|
+
path: 'server/index.ts',
|
|
38
|
+
content: `import express from 'express';
|
|
39
|
+
import cors from 'cors';
|
|
40
|
+
${routeImports}
|
|
41
|
+
|
|
42
|
+
const app = express();
|
|
43
|
+
const PORT = process.env.PORT ?? 3001;
|
|
44
|
+
|
|
45
|
+
app.use(cors());
|
|
46
|
+
app.use(express.json());
|
|
47
|
+
|
|
48
|
+
${routeMounts}
|
|
49
|
+
|
|
50
|
+
app.listen(PORT, () => {
|
|
51
|
+
console.log(\`API server running on http://localhost:\${String(PORT)}\`);
|
|
52
|
+
});
|
|
53
|
+
`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Data Store
|
|
58
|
+
// =============================================================================
|
|
59
|
+
function generateDataStore(model, entity) {
|
|
60
|
+
const fileName = toKebabCase(entity.pluralName).toLowerCase();
|
|
61
|
+
const varName = toCamelCase(entity.pluralName);
|
|
62
|
+
const typeName = entity.name;
|
|
63
|
+
// Generate type definition for the store
|
|
64
|
+
const typeFields = [' id: number;'];
|
|
65
|
+
for (const field of entity.fields) {
|
|
66
|
+
const optional = field.required ? '' : '?';
|
|
67
|
+
typeFields.push(` ${field.name}${optional}: ${tsType(field)};`);
|
|
68
|
+
}
|
|
69
|
+
for (const rel of belongsToRels(entity)) {
|
|
70
|
+
typeFields.push(` ${fkFieldName(rel)}: number;`);
|
|
71
|
+
}
|
|
72
|
+
const seedData = generateSeedData(model, entity);
|
|
73
|
+
return {
|
|
74
|
+
path: `server/data/${fileName}.ts`,
|
|
75
|
+
content: `// In-memory data store for ${entity.pluralName}
|
|
76
|
+
|
|
77
|
+
export interface ${typeName}Record {
|
|
78
|
+
${typeFields.join('\n')}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
${seedData}
|
|
82
|
+
|
|
83
|
+
let items: ${typeName}Record[] = [...${varName}SeedData];
|
|
84
|
+
let nextId = items.length + 1;
|
|
85
|
+
|
|
86
|
+
export function getAll(): ${typeName}Record[] {
|
|
87
|
+
return items;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getById(id: number): ${typeName}Record | undefined {
|
|
91
|
+
return items.find((item) => item.id === id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function create(data: Omit<${typeName}Record, 'id'>): ${typeName}Record {
|
|
95
|
+
const item = { ...data, id: nextId++ };
|
|
96
|
+
items.push(item);
|
|
97
|
+
return item;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function update(id: number, data: Partial<${typeName}Record>): ${typeName}Record | undefined {
|
|
101
|
+
const index = items.findIndex((item) => item.id === id);
|
|
102
|
+
if (index === -1) return undefined;
|
|
103
|
+
items[index] = { ...items[index], ...data, id };
|
|
104
|
+
return items[index];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function remove(id: number): boolean {
|
|
108
|
+
const index = items.findIndex((item) => item.id === id);
|
|
109
|
+
if (index === -1) return false;
|
|
110
|
+
items.splice(index, 1);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Routes
|
|
118
|
+
// =============================================================================
|
|
119
|
+
function generateRoutes(model, entity) {
|
|
120
|
+
const fileName = toKebabCase(entity.pluralName).toLowerCase();
|
|
121
|
+
const varName = toCamelCase(entity.pluralName) + 'Router';
|
|
122
|
+
const dataImport = fileName;
|
|
123
|
+
const btoRels = belongsToRels(entity);
|
|
124
|
+
// Populate belongsTo entities
|
|
125
|
+
const populateImports = btoRels
|
|
126
|
+
.map((rel) => {
|
|
127
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
128
|
+
if (!targetEntity)
|
|
129
|
+
return '';
|
|
130
|
+
const targetFile = toKebabCase(targetEntity.pluralName).toLowerCase();
|
|
131
|
+
return `import { getById as get${rel.target}ById } from '../data/${targetFile}.js';`;
|
|
132
|
+
})
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join('\n');
|
|
135
|
+
const populateLogic = btoRels.length > 0
|
|
136
|
+
? `
|
|
137
|
+
function populate(item: Record<string, unknown>): Record<string, unknown> {
|
|
138
|
+
const result = { ...item };
|
|
139
|
+
${btoRels
|
|
140
|
+
.map((rel) => {
|
|
141
|
+
const fk = fkFieldName(rel);
|
|
142
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
143
|
+
return ` if (result.${fk}) result.${targetVar} = get${rel.target}ById(result.${fk} as number);`;
|
|
144
|
+
})
|
|
145
|
+
.join('\n')}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
`
|
|
149
|
+
: '';
|
|
150
|
+
const getListReturn = btoRels.length > 0
|
|
151
|
+
? 'res.json(items.map((item) => populate(item as unknown as Record<string, unknown>)));'
|
|
152
|
+
: 'res.json(items);';
|
|
153
|
+
const getByIdReturn = btoRels.length > 0
|
|
154
|
+
? 'res.json(populate(item as unknown as Record<string, unknown>));'
|
|
155
|
+
: 'res.json(item);';
|
|
156
|
+
return {
|
|
157
|
+
path: `server/routes/${fileName}.ts`,
|
|
158
|
+
content: `import { Router } from 'express';
|
|
159
|
+
import { getAll, getById, create, update, remove } from '../data/${dataImport}.js';
|
|
160
|
+
${populateImports}
|
|
161
|
+
|
|
162
|
+
export const ${varName} = Router();
|
|
163
|
+
${populateLogic}
|
|
164
|
+
// GET all
|
|
165
|
+
${varName}.get('/', (_req, res) => {
|
|
166
|
+
const items = getAll();
|
|
167
|
+
${getListReturn}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// GET by ID
|
|
171
|
+
${varName}.get('/:id', (req, res) => {
|
|
172
|
+
const item = getById(Number(req.params.id));
|
|
173
|
+
if (!item) return res.status(404).json({ error: 'Not found' });
|
|
174
|
+
${getByIdReturn}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// POST create
|
|
178
|
+
${varName}.post('/', (req, res) => {
|
|
179
|
+
const item = create(req.body);
|
|
180
|
+
res.status(201).json(item);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// PUT update
|
|
184
|
+
${varName}.put('/:id', (req, res) => {
|
|
185
|
+
const item = update(Number(req.params.id), req.body);
|
|
186
|
+
if (!item) return res.status(404).json({ error: 'Not found' });
|
|
187
|
+
res.json(item);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// DELETE
|
|
191
|
+
${varName}.delete('/:id', (req, res) => {
|
|
192
|
+
const success = remove(Number(req.params.id));
|
|
193
|
+
if (!success) return res.status(404).json({ error: 'Not found' });
|
|
194
|
+
res.status(204).send();
|
|
195
|
+
});
|
|
196
|
+
`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js
|
|
6
|
+
*/
|
|
7
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
8
|
+
import type { FactoryFile } from '../types.js';
|
|
9
|
+
export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
|