@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.
Files changed (64) hide show
  1. package/README.md +131 -0
  2. package/dist/factory/file-writer.d.ts +11 -0
  3. package/dist/factory/file-writer.js +21 -0
  4. package/dist/factory/list-toolkits-tool.d.ts +12 -0
  5. package/dist/factory/list-toolkits-tool.js +35 -0
  6. package/dist/factory/registry.d.ts +15 -0
  7. package/dist/factory/registry.js +28 -0
  8. package/dist/factory/scaffold-tool.d.ts +21 -0
  9. package/dist/factory/scaffold-tool.js +95 -0
  10. package/dist/factory/skill.d.ts +14 -0
  11. package/dist/factory/skill.js +146 -0
  12. package/dist/factory/tools.d.ts +14 -0
  13. package/dist/factory/tools.js +17 -0
  14. package/dist/index.d.ts +22 -0
  15. package/dist/index.js +25 -0
  16. package/dist/model/defaults.d.ts +10 -0
  17. package/dist/model/defaults.js +68 -0
  18. package/dist/model/naming.d.ts +12 -0
  19. package/dist/model/naming.js +80 -0
  20. package/dist/model/operations-entity.d.ts +35 -0
  21. package/dist/model/operations-entity.js +110 -0
  22. package/dist/model/operations-field.d.ts +33 -0
  23. package/dist/model/operations-field.js +104 -0
  24. package/dist/model/operations-relationship.d.ts +19 -0
  25. package/dist/model/operations-relationship.js +90 -0
  26. package/dist/model/operations-section.d.ts +32 -0
  27. package/dist/model/operations-section.js +35 -0
  28. package/dist/model/operations.d.ts +12 -0
  29. package/dist/model/operations.js +63 -0
  30. package/dist/model/persistence.d.ts +19 -0
  31. package/dist/model/persistence.js +40 -0
  32. package/dist/model/schema.d.ts +15 -0
  33. package/dist/model/schema.js +269 -0
  34. package/dist/model/tools.d.ts +14 -0
  35. package/dist/model/tools.js +380 -0
  36. package/dist/model/types.d.ts +70 -0
  37. package/dist/model/types.js +8 -0
  38. package/dist/toolkits/react-node/api.d.ts +8 -0
  39. package/dist/toolkits/react-node/api.js +198 -0
  40. package/dist/toolkits/react-node/config.d.ts +9 -0
  41. package/dist/toolkits/react-node/config.js +120 -0
  42. package/dist/toolkits/react-node/dashboard.d.ts +8 -0
  43. package/dist/toolkits/react-node/dashboard.js +60 -0
  44. package/dist/toolkits/react-node/entity.d.ts +8 -0
  45. package/dist/toolkits/react-node/entity.js +469 -0
  46. package/dist/toolkits/react-node/helpers.d.ts +27 -0
  47. package/dist/toolkits/react-node/helpers.js +71 -0
  48. package/dist/toolkits/react-node/index.d.ts +8 -0
  49. package/dist/toolkits/react-node/index.js +52 -0
  50. package/dist/toolkits/react-node/router.d.ts +8 -0
  51. package/dist/toolkits/react-node/router.js +49 -0
  52. package/dist/toolkits/react-node/seed.d.ts +11 -0
  53. package/dist/toolkits/react-node/seed.js +144 -0
  54. package/dist/toolkits/react-node/shared.d.ts +7 -0
  55. package/dist/toolkits/react-node/shared.js +119 -0
  56. package/dist/toolkits/react-node/shell.d.ts +8 -0
  57. package/dist/toolkits/react-node/shell.js +152 -0
  58. package/dist/toolkits/react-node/static.d.ts +8 -0
  59. package/dist/toolkits/react-node/static.js +105 -0
  60. package/dist/toolkits/react-node/types-gen.d.ts +8 -0
  61. package/dist/toolkits/react-node/types-gen.js +31 -0
  62. package/dist/toolkits/types.d.ts +22 -0
  63. package/dist/toolkits/types.js +6 -0
  64. package/package.json +51 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Default Model Creators
3
+ *
4
+ * Factory functions for creating ApplicationModel and Entity instances
5
+ * with sensible defaults.
6
+ */
7
+ import { toPlural } from './naming.js';
8
+ const FACTORY_VERSION = '0.1.0';
9
+ export function createDefaultModel(overrides) {
10
+ const now = new Date().toISOString();
11
+ return {
12
+ identity: {
13
+ name: 'My Application',
14
+ description: 'A new application',
15
+ version: '1.0.0',
16
+ ...overrides?.identity,
17
+ },
18
+ entities: overrides?.entities ?? [],
19
+ layout: {
20
+ shell: 'sidebar-header',
21
+ ...overrides?.layout,
22
+ },
23
+ features: {
24
+ dashboard: true,
25
+ auth: false,
26
+ userProfiles: false,
27
+ settings: false,
28
+ darkMode: false,
29
+ ...overrides?.features,
30
+ },
31
+ theme: {
32
+ primaryColor: '#3B82F6',
33
+ ...overrides?.theme,
34
+ },
35
+ techStack: {
36
+ toolkit: 'react-node',
37
+ ...overrides?.techStack,
38
+ },
39
+ meta: {
40
+ revision: 1,
41
+ createdAt: now,
42
+ updatedAt: now,
43
+ factoryVersion: FACTORY_VERSION,
44
+ ...overrides?.meta,
45
+ },
46
+ };
47
+ }
48
+ export function createDefaultEntity(name, overrides) {
49
+ const entityName = overrides?.name ?? name;
50
+ return {
51
+ name: entityName,
52
+ pluralName: overrides?.pluralName ?? toPlural(entityName),
53
+ icon: overrides?.icon ?? '📄',
54
+ fields: overrides?.fields ?? [],
55
+ views: overrides?.views ?? ['list', 'detail'],
56
+ relationships: overrides?.relationships ?? [],
57
+ description: overrides?.description,
58
+ };
59
+ }
60
+ export function createDefaultField(name, label, overrides) {
61
+ return {
62
+ name,
63
+ label,
64
+ type: 'string',
65
+ required: false,
66
+ ...overrides,
67
+ };
68
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Naming Utilities
3
+ *
4
+ * Converts between naming conventions used in the Application Model.
5
+ */
6
+ export declare function toPascalCase(str: string): string;
7
+ export declare function toCamelCase(str: string): string;
8
+ export declare function toKebabCase(str: string): string;
9
+ export declare function toPlural(str: string): string;
10
+ export declare function isPascalCase(str: string): boolean;
11
+ export declare function isCamelCase(str: string): boolean;
12
+ export declare function isValidHexColor(str: string): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Naming Utilities
3
+ *
4
+ * Converts between naming conventions used in the Application Model.
5
+ */
6
+ const IRREGULAR_PLURALS = {
7
+ person: 'people',
8
+ child: 'children',
9
+ mouse: 'mice',
10
+ goose: 'geese',
11
+ man: 'men',
12
+ woman: 'women',
13
+ tooth: 'teeth',
14
+ foot: 'feet',
15
+ datum: 'data',
16
+ analysis: 'analyses',
17
+ crisis: 'crises',
18
+ criterion: 'criteria',
19
+ phenomenon: 'phenomena',
20
+ index: 'indices',
21
+ matrix: 'matrices',
22
+ vertex: 'vertices',
23
+ appendix: 'appendices',
24
+ ox: 'oxen',
25
+ quiz: 'quizzes',
26
+ status: 'statuses',
27
+ bus: 'buses',
28
+ };
29
+ export function toPascalCase(str) {
30
+ return str
31
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
32
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
33
+ }
34
+ export function toCamelCase(str) {
35
+ const pascal = toPascalCase(str);
36
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
37
+ }
38
+ export function toKebabCase(str) {
39
+ return str
40
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
41
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
42
+ .replace(/[\s_]+/g, '-')
43
+ .toLowerCase();
44
+ }
45
+ export function toPlural(str) {
46
+ const lower = str.toLowerCase();
47
+ if (IRREGULAR_PLURALS[lower]) {
48
+ const plural = IRREGULAR_PLURALS[lower];
49
+ if (str[0] === str[0].toUpperCase()) {
50
+ return plural.charAt(0).toUpperCase() + plural.slice(1);
51
+ }
52
+ return plural;
53
+ }
54
+ if (str.endsWith('s') ||
55
+ str.endsWith('sh') ||
56
+ str.endsWith('ch') ||
57
+ str.endsWith('x') ||
58
+ str.endsWith('z')) {
59
+ return str + 'es';
60
+ }
61
+ if (str.endsWith('y') && !/[aeiou]y$/i.test(str)) {
62
+ return str.slice(0, -1) + 'ies';
63
+ }
64
+ if (str.endsWith('f')) {
65
+ return str.slice(0, -1) + 'ves';
66
+ }
67
+ if (str.endsWith('fe')) {
68
+ return str.slice(0, -2) + 'ves';
69
+ }
70
+ return str + 's';
71
+ }
72
+ export function isPascalCase(str) {
73
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str);
74
+ }
75
+ export function isCamelCase(str) {
76
+ return /^[a-z][a-zA-Z0-9]*$/.test(str);
77
+ }
78
+ export function isValidHexColor(str) {
79
+ return /^#[0-9A-Fa-f]{6}$/.test(str);
80
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Entity Operations
3
+ *
4
+ * addEntity, updateEntity, removeEntity, renameEntity, reorderEntities
5
+ */
6
+ import type { ApplicationModel, Entity } from './types.js';
7
+ export interface AddEntityOp {
8
+ readonly op: 'addEntity';
9
+ readonly entity: Entity;
10
+ }
11
+ export interface UpdateEntityOp {
12
+ readonly op: 'updateEntity';
13
+ readonly entity: string;
14
+ readonly updates: Partial<Omit<Entity, 'name' | 'relationships'>>;
15
+ }
16
+ export interface RemoveEntityOp {
17
+ readonly op: 'removeEntity';
18
+ readonly entity: string;
19
+ readonly force?: boolean;
20
+ }
21
+ export interface RenameEntityOp {
22
+ readonly op: 'renameEntity';
23
+ readonly entity: string;
24
+ readonly newName: string;
25
+ }
26
+ export interface ReorderEntitiesOp {
27
+ readonly op: 'reorderEntities';
28
+ readonly order: readonly string[];
29
+ }
30
+ export type EntityOperation = AddEntityOp | UpdateEntityOp | RemoveEntityOp | RenameEntityOp | ReorderEntitiesOp;
31
+ export declare function addEntity(model: ApplicationModel, op: AddEntityOp): ApplicationModel;
32
+ export declare function updateEntity(model: ApplicationModel, op: UpdateEntityOp): ApplicationModel;
33
+ export declare function removeEntity(model: ApplicationModel, op: RemoveEntityOp): ApplicationModel;
34
+ export declare function renameEntity(model: ApplicationModel, op: RenameEntityOp): ApplicationModel;
35
+ export declare function reorderEntities(model: ApplicationModel, op: ReorderEntitiesOp): ApplicationModel;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Entity Operations
3
+ *
4
+ * addEntity, updateEntity, removeEntity, renameEntity, reorderEntities
5
+ */
6
+ import { isPascalCase } from './naming.js';
7
+ function findEntity(model, name) {
8
+ const entity = model.entities.find((e) => e.name === name);
9
+ if (!entity) {
10
+ throw new Error(`Entity "${name}" not found`);
11
+ }
12
+ return entity;
13
+ }
14
+ export function addEntity(model, op) {
15
+ const { entity } = op;
16
+ if (!entity.name || !isPascalCase(entity.name)) {
17
+ throw new Error(`Entity name "${entity.name}" must be non-empty PascalCase`);
18
+ }
19
+ if (model.entities.some((e) => e.name === entity.name)) {
20
+ throw new Error(`Entity "${entity.name}" already exists`);
21
+ }
22
+ return {
23
+ ...model,
24
+ entities: [...model.entities, entity],
25
+ };
26
+ }
27
+ export function updateEntity(model, op) {
28
+ findEntity(model, op.entity);
29
+ return {
30
+ ...model,
31
+ entities: model.entities.map((e) => {
32
+ if (e.name !== op.entity)
33
+ return e;
34
+ return {
35
+ ...e,
36
+ ...op.updates,
37
+ name: e.name,
38
+ relationships: e.relationships,
39
+ };
40
+ }),
41
+ };
42
+ }
43
+ export function removeEntity(model, op) {
44
+ findEntity(model, op.entity);
45
+ const referencingEntities = model.entities.filter((e) => e.name !== op.entity && e.relationships.some((r) => r.target === op.entity));
46
+ if (referencingEntities.length > 0 && !op.force) {
47
+ const names = referencingEntities.map((e) => e.name).join(', ');
48
+ throw new Error(`Cannot remove "${op.entity}": referenced by ${names}. Use force: true to cascade.`);
49
+ }
50
+ let entities = model.entities.filter((e) => e.name !== op.entity);
51
+ if (op.force) {
52
+ entities = entities.map((e) => ({
53
+ ...e,
54
+ relationships: e.relationships.filter((r) => r.target !== op.entity),
55
+ }));
56
+ }
57
+ return { ...model, entities };
58
+ }
59
+ export function renameEntity(model, op) {
60
+ findEntity(model, op.entity);
61
+ if (!op.newName || !isPascalCase(op.newName)) {
62
+ throw new Error(`New entity name "${op.newName}" must be non-empty PascalCase`);
63
+ }
64
+ if (op.entity !== op.newName && model.entities.some((e) => e.name === op.newName)) {
65
+ throw new Error(`Entity "${op.newName}" already exists`);
66
+ }
67
+ return {
68
+ ...model,
69
+ entities: model.entities.map((e) => {
70
+ let entity = e;
71
+ // Rename the entity itself
72
+ if (entity.name === op.entity) {
73
+ entity = { ...entity, name: op.newName };
74
+ }
75
+ // Update relationship targets
76
+ const hasTargetRef = entity.relationships.some((r) => r.target === op.entity);
77
+ if (hasTargetRef) {
78
+ entity = {
79
+ ...entity,
80
+ relationships: entity.relationships.map((r) => {
81
+ if (r.target !== op.entity)
82
+ return r;
83
+ return { ...r, target: op.newName };
84
+ }),
85
+ };
86
+ }
87
+ return entity;
88
+ }),
89
+ };
90
+ }
91
+ export function reorderEntities(model, op) {
92
+ const currentNames = model.entities.map((e) => e.name);
93
+ const orderNames = [...op.order];
94
+ if (orderNames.length !== currentNames.length) {
95
+ throw new Error(`Order must contain exactly ${String(currentNames.length)} entities, got ${String(orderNames.length)}`);
96
+ }
97
+ for (const name of orderNames) {
98
+ if (!currentNames.includes(name)) {
99
+ throw new Error(`Entity "${name}" in order does not exist`);
100
+ }
101
+ }
102
+ const entityMap = new Map(model.entities.map((e) => [e.name, e]));
103
+ const reordered = orderNames.map((name) => {
104
+ const entity = entityMap.get(name);
105
+ if (!entity)
106
+ throw new Error(`Entity "${name}" in order does not exist`);
107
+ return entity;
108
+ });
109
+ return { ...model, entities: reordered };
110
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Field Operations
3
+ *
4
+ * addField, updateField, removeField, renameField
5
+ */
6
+ import type { ApplicationModel, Field } from './types.js';
7
+ export interface AddFieldOp {
8
+ readonly op: 'addField';
9
+ readonly entity: string;
10
+ readonly field: Field;
11
+ }
12
+ export interface UpdateFieldOp {
13
+ readonly op: 'updateField';
14
+ readonly entity: string;
15
+ readonly field: string;
16
+ readonly updates: Partial<Omit<Field, 'name'>>;
17
+ }
18
+ export interface RemoveFieldOp {
19
+ readonly op: 'removeField';
20
+ readonly entity: string;
21
+ readonly field: string;
22
+ }
23
+ export interface RenameFieldOp {
24
+ readonly op: 'renameField';
25
+ readonly entity: string;
26
+ readonly field: string;
27
+ readonly newName: string;
28
+ }
29
+ export type FieldOperation = AddFieldOp | UpdateFieldOp | RemoveFieldOp | RenameFieldOp;
30
+ export declare function addField(model: ApplicationModel, op: AddFieldOp): ApplicationModel;
31
+ export declare function updateField(model: ApplicationModel, op: UpdateFieldOp): ApplicationModel;
32
+ export declare function removeField(model: ApplicationModel, op: RemoveFieldOp): ApplicationModel;
33
+ export declare function renameField(model: ApplicationModel, op: RenameFieldOp): ApplicationModel;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Field Operations
3
+ *
4
+ * addField, updateField, removeField, renameField
5
+ */
6
+ import { isCamelCase } from './naming.js';
7
+ function findEntity(model, name) {
8
+ const index = model.entities.findIndex((e) => e.name === name);
9
+ if (index === -1) {
10
+ throw new Error(`Entity "${name}" not found`);
11
+ }
12
+ return index;
13
+ }
14
+ function findField(model, entityName, fieldName) {
15
+ const entityIdx = findEntity(model, entityName);
16
+ const entity = model.entities[entityIdx];
17
+ const fieldIdx = entity.fields.findIndex((f) => f.name === fieldName);
18
+ if (fieldIdx === -1) {
19
+ throw new Error(`Field "${fieldName}" not found in entity "${entityName}"`);
20
+ }
21
+ return fieldIdx;
22
+ }
23
+ function updateEntityAt(model, entityIdx, updater) {
24
+ return {
25
+ ...model,
26
+ entities: model.entities.map((e, i) => {
27
+ if (i !== entityIdx)
28
+ return e;
29
+ return { ...e, fields: updater(e.fields) };
30
+ }),
31
+ };
32
+ }
33
+ export function addField(model, op) {
34
+ const entityIdx = findEntity(model, op.entity);
35
+ const entity = model.entities[entityIdx];
36
+ if (!op.field.name || !isCamelCase(op.field.name)) {
37
+ throw new Error(`Field name "${op.field.name}" must be non-empty camelCase`);
38
+ }
39
+ if (entity.fields.some((f) => f.name === op.field.name)) {
40
+ throw new Error(`Field "${op.field.name}" already exists in entity "${op.entity}"`);
41
+ }
42
+ return updateEntityAt(model, entityIdx, (fields) => [...fields, op.field]);
43
+ }
44
+ export function updateField(model, op) {
45
+ const entityIdx = findEntity(model, op.entity);
46
+ findField(model, op.entity, op.field);
47
+ return updateEntityAt(model, entityIdx, (fields) => fields.map((f) => {
48
+ if (f.name !== op.field)
49
+ return f;
50
+ return { ...f, ...op.updates, name: f.name };
51
+ }));
52
+ }
53
+ export function removeField(model, op) {
54
+ const entityIdx = findEntity(model, op.entity);
55
+ findField(model, op.entity, op.field);
56
+ // Check if field is a FK field
57
+ const entity = model.entities[entityIdx];
58
+ const isFk = entity.relationships.some((r) => {
59
+ const fkField = r.fieldName ?? `${r.target.charAt(0).toLowerCase()}${r.target.slice(1)}Id`;
60
+ return r.type === 'belongsTo' && fkField === op.field;
61
+ });
62
+ if (isFk) {
63
+ throw new Error(`Cannot remove field "${op.field}": it is a foreign key. Remove the relationship first.`);
64
+ }
65
+ return updateEntityAt(model, entityIdx, (fields) => fields.filter((f) => f.name !== op.field));
66
+ }
67
+ export function renameField(model, op) {
68
+ const entityIdx = findEntity(model, op.entity);
69
+ findField(model, op.entity, op.field);
70
+ if (!op.newName || !isCamelCase(op.newName)) {
71
+ throw new Error(`New field name "${op.newName}" must be non-empty camelCase`);
72
+ }
73
+ const entity = model.entities[entityIdx];
74
+ if (op.field !== op.newName && entity.fields.some((f) => f.name === op.newName)) {
75
+ throw new Error(`Field "${op.newName}" already exists in entity "${op.entity}"`);
76
+ }
77
+ // Rename the field
78
+ let result = updateEntityAt(model, entityIdx, (fields) => fields.map((f) => {
79
+ if (f.name !== op.field)
80
+ return f;
81
+ return { ...f, name: op.newName };
82
+ }));
83
+ // Update relationship fieldName if this was a FK field
84
+ result = {
85
+ ...result,
86
+ entities: result.entities.map((e) => {
87
+ if (e.name !== op.entity)
88
+ return e;
89
+ return {
90
+ ...e,
91
+ relationships: e.relationships.map((r) => {
92
+ if (r.type !== 'belongsTo')
93
+ return r;
94
+ const defaultFk = `${r.target.charAt(0).toLowerCase()}${r.target.slice(1)}Id`;
95
+ const currentFk = r.fieldName ?? defaultFk;
96
+ if (currentFk !== op.field)
97
+ return r;
98
+ return { ...r, fieldName: op.newName };
99
+ }),
100
+ };
101
+ }),
102
+ };
103
+ return result;
104
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Relationship Operations
3
+ *
4
+ * addRelationship (with auto-inverse), removeRelationship (with auto-inverse cleanup)
5
+ */
6
+ import type { ApplicationModel, Relationship } from './types.js';
7
+ export interface AddRelationshipOp {
8
+ readonly op: 'addRelationship';
9
+ readonly entity: string;
10
+ readonly relationship: Relationship;
11
+ }
12
+ export interface RemoveRelationshipOp {
13
+ readonly op: 'removeRelationship';
14
+ readonly entity: string;
15
+ readonly target: string;
16
+ }
17
+ export type RelationshipOperation = AddRelationshipOp | RemoveRelationshipOp;
18
+ export declare function addRelationship(model: ApplicationModel, op: AddRelationshipOp): ApplicationModel;
19
+ export declare function removeRelationship(model: ApplicationModel, op: RemoveRelationshipOp): ApplicationModel;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Relationship Operations
3
+ *
4
+ * addRelationship (with auto-inverse), removeRelationship (with auto-inverse cleanup)
5
+ */
6
+ function getEntity(model, name) {
7
+ const entity = model.entities.find((e) => e.name === name);
8
+ if (!entity) {
9
+ throw new Error(`Entity "${name}" not found`);
10
+ }
11
+ return entity;
12
+ }
13
+ function addInverse(result, targetName, inverseType, inverseTarget) {
14
+ const targetEntity = getEntity(result, targetName);
15
+ const hasInverse = targetEntity.relationships.some((r) => r.type === inverseType && r.target === inverseTarget);
16
+ if (hasInverse)
17
+ return result;
18
+ return {
19
+ ...result,
20
+ entities: result.entities.map((e) => {
21
+ if (e.name !== targetName)
22
+ return e;
23
+ return {
24
+ ...e,
25
+ relationships: [...e.relationships, { type: inverseType, target: inverseTarget }],
26
+ };
27
+ }),
28
+ };
29
+ }
30
+ export function addRelationship(model, op) {
31
+ getEntity(model, op.entity);
32
+ getEntity(model, op.relationship.target);
33
+ if (op.relationship.target === op.entity) {
34
+ throw new Error('Self-referential relationships are not supported');
35
+ }
36
+ // Check for duplicate
37
+ const entity = getEntity(model, op.entity);
38
+ const exists = entity.relationships.some((r) => r.type === op.relationship.type && r.target === op.relationship.target);
39
+ if (exists) {
40
+ throw new Error(`Relationship ${op.relationship.type} → ${op.relationship.target} already exists on "${op.entity}"`);
41
+ }
42
+ let result = {
43
+ ...model,
44
+ entities: model.entities.map((e) => {
45
+ if (e.name !== op.entity)
46
+ return e;
47
+ return {
48
+ ...e,
49
+ relationships: [...e.relationships, op.relationship],
50
+ };
51
+ }),
52
+ };
53
+ // Auto-add inverse
54
+ const inverseType = op.relationship.type === 'belongsTo' ? 'hasMany' : 'belongsTo';
55
+ result = addInverse(result, op.relationship.target, inverseType, op.entity);
56
+ return result;
57
+ }
58
+ export function removeRelationship(model, op) {
59
+ const entity = getEntity(model, op.entity);
60
+ const rel = entity.relationships.find((r) => r.target === op.target);
61
+ if (!rel) {
62
+ throw new Error(`No relationship to "${op.target}" found on entity "${op.entity}"`);
63
+ }
64
+ // Remove the relationship
65
+ let result = {
66
+ ...model,
67
+ entities: model.entities.map((e) => {
68
+ if (e.name !== op.entity)
69
+ return e;
70
+ return {
71
+ ...e,
72
+ relationships: e.relationships.filter((r) => r.target !== op.target),
73
+ };
74
+ }),
75
+ };
76
+ // Auto-remove inverse
77
+ const inverseType = rel.type === 'belongsTo' ? 'hasMany' : 'belongsTo';
78
+ result = {
79
+ ...result,
80
+ entities: result.entities.map((e) => {
81
+ if (e.name !== op.target)
82
+ return e;
83
+ return {
84
+ ...e,
85
+ relationships: e.relationships.filter((r) => !(r.type === inverseType && r.target === op.entity)),
86
+ };
87
+ }),
88
+ };
89
+ return result;
90
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Section Operations
3
+ *
4
+ * updateIdentity, updateLayout, updateFeatures, updateTheme, updateTechStack
5
+ */
6
+ import type { ApplicationModel, Features, Identity, Layout, TechStack, Theme } from './types.js';
7
+ export interface UpdateIdentityOp {
8
+ readonly op: 'updateIdentity';
9
+ readonly updates: Partial<Identity>;
10
+ }
11
+ export interface UpdateLayoutOp {
12
+ readonly op: 'updateLayout';
13
+ readonly updates: Partial<Layout>;
14
+ }
15
+ export interface UpdateFeaturesOp {
16
+ readonly op: 'updateFeatures';
17
+ readonly updates: Partial<Features>;
18
+ }
19
+ export interface UpdateThemeOp {
20
+ readonly op: 'updateTheme';
21
+ readonly updates: Partial<Theme>;
22
+ }
23
+ export interface UpdateTechStackOp {
24
+ readonly op: 'updateTechStack';
25
+ readonly updates: Partial<TechStack>;
26
+ }
27
+ export type SectionOperation = UpdateIdentityOp | UpdateLayoutOp | UpdateFeaturesOp | UpdateThemeOp | UpdateTechStackOp;
28
+ export declare function updateIdentity(model: ApplicationModel, op: UpdateIdentityOp): ApplicationModel;
29
+ export declare function updateLayout(model: ApplicationModel, op: UpdateLayoutOp): ApplicationModel;
30
+ export declare function updateFeatures(model: ApplicationModel, op: UpdateFeaturesOp): ApplicationModel;
31
+ export declare function updateTheme(model: ApplicationModel, op: UpdateThemeOp): ApplicationModel;
32
+ export declare function updateTechStack(model: ApplicationModel, op: UpdateTechStackOp): ApplicationModel;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Section Operations
3
+ *
4
+ * updateIdentity, updateLayout, updateFeatures, updateTheme, updateTechStack
5
+ */
6
+ export function updateIdentity(model, op) {
7
+ return {
8
+ ...model,
9
+ identity: { ...model.identity, ...op.updates },
10
+ };
11
+ }
12
+ export function updateLayout(model, op) {
13
+ return {
14
+ ...model,
15
+ layout: { ...model.layout, ...op.updates },
16
+ };
17
+ }
18
+ export function updateFeatures(model, op) {
19
+ return {
20
+ ...model,
21
+ features: { ...model.features, ...op.updates },
22
+ };
23
+ }
24
+ export function updateTheme(model, op) {
25
+ return {
26
+ ...model,
27
+ theme: { ...model.theme, ...op.updates },
28
+ };
29
+ }
30
+ export function updateTechStack(model, op) {
31
+ return {
32
+ ...model,
33
+ techStack: { ...model.techStack, ...op.updates },
34
+ };
35
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Operation Dispatcher
3
+ *
4
+ * Routes op → handler, manages revision + updatedAt.
5
+ */
6
+ import type { ApplicationModel } from './types.js';
7
+ import { type EntityOperation } from './operations-entity.js';
8
+ import { type FieldOperation } from './operations-field.js';
9
+ import { type RelationshipOperation } from './operations-relationship.js';
10
+ import { type SectionOperation } from './operations-section.js';
11
+ export type ModelOperation = EntityOperation | FieldOperation | RelationshipOperation | SectionOperation;
12
+ export declare function applyOperation(model: ApplicationModel, operation: ModelOperation): ApplicationModel;