dzql 0.5.33 → 0.6.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 (142) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +309 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +653 -0
  20. package/docs/project-setup.md +456 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. package/src/server/ws.js +0 -573
@@ -1,553 +0,0 @@
1
- /**
2
- * Drop Semantics Code Generator
3
- * Generates a JSON manifest describing valid drag-and-drop interactions for a canvas UI
4
- *
5
- * Terminology clarification:
6
- * - "source" = the entity being dragged
7
- * - "target" = the entity being dropped onto
8
- *
9
- * Derivation rules:
10
- * 1. FK on entity A pointing to entity B (e.g., tasks.group_id REFERENCES task_groups):
11
- * - A.droppable_on.B: drag A onto B → update A.group_id = B.id
12
- * - A.accepts.B: drag B onto A → update A.group_id = B.id (same operation, different drag direction)
13
- *
14
- * 2. Junction table (M2M):
15
- * - Both entities are droppable_on each other via junction insert
16
- * - Both entities accept each other
17
- *
18
- * 3. Self-referential FK:
19
- * - Entity is droppable on itself
20
- *
21
- * Visual semantics:
22
- * - "containment": node moves inside container (tree structures, folders)
23
- * - "frame": visual bounding box around members (sets, collections)
24
- * - "edge": arrow drawn between nodes (dependencies, relationships)
25
- * - "badge": tag/chip displayed on node (assignments, references)
26
- */
27
-
28
- export class DropSemanticsCodegen {
29
- /**
30
- * @param {Object} entities - Map of tableName -> entityConfig
31
- */
32
- constructor(entities) {
33
- this.entities = entities;
34
- }
35
-
36
- /**
37
- * Generate the complete drop semantics manifest
38
- * @returns {Object} Drop semantics JSON structure
39
- */
40
- generate() {
41
- // Initialize result structure for all entities
42
- const semantics = {};
43
- for (const tableName of Object.keys(this.entities)) {
44
- semantics[tableName] = {
45
- droppable_on: {},
46
- accepts: {}
47
- };
48
- }
49
-
50
- // Process all FK relationships (adds to source's droppable_on and accepts)
51
- for (const [tableName, config] of Object.entries(this.entities)) {
52
- this._processFKRelationships(tableName, config, semantics);
53
- }
54
-
55
- // Process all M2M relationships (adds to source's droppable_on and accepts)
56
- for (const [tableName, config] of Object.entries(this.entities)) {
57
- this._processM2MRelationships(tableName, config, semantics);
58
- }
59
-
60
- // Second pass: populate target's accepts from source's droppable_on
61
- // This ensures that if posts.droppable_on.tags exists, tags.accepts.posts also exists
62
- this._populateTargetAccepts(semantics);
63
-
64
- // Filter out entities with no semantics
65
- const result = { entities: {} };
66
- for (const [tableName, sem] of Object.entries(semantics)) {
67
- if (Object.keys(sem.droppable_on).length > 0 || Object.keys(sem.accepts).length > 0) {
68
- result.entities[tableName] = sem;
69
- }
70
- }
71
-
72
- return result;
73
- }
74
-
75
- /**
76
- * Populate target's accepts from source's droppable_on
77
- * If A.droppable_on.B exists, then B.accepts.A should also exist
78
- * @private
79
- */
80
- _populateTargetAccepts(semantics) {
81
- for (const [sourceTable, sem] of Object.entries(semantics)) {
82
- for (const [targetTable, actions] of Object.entries(sem.droppable_on)) {
83
- // Skip if target doesn't exist in our entities
84
- if (!semantics[targetTable]) continue;
85
-
86
- // For each droppable_on action, create a corresponding accepts entry
87
- for (const action of actions) {
88
- if (!semantics[targetTable].accepts[sourceTable]) {
89
- semantics[targetTable].accepts[sourceTable] = [];
90
- }
91
-
92
- // Check if this exact relation already exists (avoid duplicates)
93
- const exists = semantics[targetTable].accepts[sourceTable].some(
94
- a => a.relation === action.relation && a.type === action.type
95
- );
96
-
97
- if (!exists) {
98
- // Create the inverse action - swap source and target in params
99
- const inverseAction = this._createInverseAction(action, sourceTable, targetTable);
100
- semantics[targetTable].accepts[sourceTable].push(inverseAction);
101
- }
102
- }
103
- }
104
- }
105
- }
106
-
107
- /**
108
- * Create an inverse action for accepts (swap source/target perspective)
109
- * @private
110
- */
111
- _createInverseAction(action, sourceTable, targetTable) {
112
- // For the inverse, @source becomes what was @target and vice versa
113
- const swapRefs = (params) => {
114
- const swapped = {};
115
- for (const [key, value] of Object.entries(params)) {
116
- if (typeof value === 'string') {
117
- swapped[key] = value
118
- .replace('@source.', '@__tmp__.')
119
- .replace('@target.', '@source.')
120
- .replace('@__tmp__.', '@target.');
121
- } else {
122
- swapped[key] = value;
123
- }
124
- }
125
- return swapped;
126
- };
127
-
128
- // For inverse (accepts), visual is typically badge unless it's an edge
129
- let inverseVisual = 'badge';
130
- if (action.visual === 'edge') {
131
- inverseVisual = 'edge'; // Edges are bidirectional visually
132
- }
133
-
134
- const result = {
135
- ...action,
136
- action: action.type === 'fk' ? 'assign' : action.action,
137
- visual: inverseVisual,
138
- label: action.type === 'fk'
139
- ? this._generateLabel(action.relation, 'assign')
140
- : action.label,
141
- operation: {
142
- ...action.operation,
143
- params: swapRefs(action.operation.params)
144
- },
145
- remove_operation: action.remove_operation ? {
146
- ...action.remove_operation,
147
- params: swapRefs(action.remove_operation.params)
148
- } : undefined
149
- };
150
-
151
- // For edge visual, swap direction
152
- if (action.direction === 'source_to_target') {
153
- result.direction = 'target_to_source';
154
- }
155
-
156
- return result;
157
- }
158
-
159
- /**
160
- * Process FK relationships for an entity
161
- * @private
162
- */
163
- _processFKRelationships(tableName, config, semantics) {
164
- const fkIncludes = config.fkIncludes || {};
165
- const primaryKey = config.primaryKey || ['id'];
166
-
167
- for (const [alias, targetTable] of Object.entries(fkIncludes)) {
168
- // Skip reverse FKs (child arrays) - indicated when alias === targetTable
169
- if (alias === targetTable) {
170
- continue;
171
- }
172
-
173
- const fkColumn = alias.endsWith('_id') ? alias : `${alias}_id`;
174
- const isSelfReferential = targetTable === tableName;
175
-
176
- // 1. Source (tableName) can be dropped onto target
177
- // e.g., tasks.droppable_on.task_groups - drag task onto group
178
- if (!semantics[tableName].droppable_on[targetTable]) {
179
- semantics[tableName].droppable_on[targetTable] = [];
180
- }
181
-
182
- const action = isSelfReferential ? this._getSelfReferentialAction(fkColumn) : 'move';
183
- const visual = this._inferVisual('fk', targetTable, isSelfReferential);
184
-
185
- // Determine if this is primarily an "accepts" relationship (assign pattern)
186
- // e.g., tasks.assigned_to_user_id - natural gesture is to drop user onto task
187
- const isAssignPattern = this._isAssignPattern(alias);
188
-
189
- semantics[tableName].droppable_on[targetTable].push({
190
- relation: fkColumn,
191
- type: 'fk',
192
- action: action,
193
- visual: visual,
194
- label: this._generateLabel(fkColumn, action),
195
- ...(isAssignPattern && { primary_direction: 'accepts' }),
196
- operation: {
197
- method: 'save',
198
- entity: tableName,
199
- params: this._buildPKParams(primaryKey, '@source', { [fkColumn]: '@target.id' })
200
- },
201
- removable: true,
202
- remove_operation: {
203
- method: 'save',
204
- entity: tableName,
205
- params: this._buildPKParams(primaryKey, '@source', { [fkColumn]: null })
206
- }
207
- });
208
-
209
- // 2. Source (tableName) accepts target being dropped on it
210
- // e.g., tasks.accepts.users - drag user onto task to assign
211
- // This only makes sense for non-self-referential FKs
212
- if (!isSelfReferential && this.entities[targetTable]) {
213
- if (!semantics[tableName].accepts[targetTable]) {
214
- semantics[tableName].accepts[targetTable] = [];
215
- }
216
-
217
- // For accepts, visual is always badge (something is being attached to this entity)
218
- semantics[tableName].accepts[targetTable].push({
219
- relation: fkColumn,
220
- type: 'fk',
221
- action: 'assign',
222
- visual: 'badge',
223
- label: this._generateLabel(alias, 'assign'),
224
- operation: {
225
- method: 'save',
226
- entity: tableName, // Update the entity with the FK
227
- params: this._buildPKParams(primaryKey, '@target', { [fkColumn]: '@source.id' })
228
- },
229
- removable: true,
230
- remove_operation: {
231
- method: 'save',
232
- entity: tableName,
233
- params: this._buildPKParams(primaryKey, '@target', { [fkColumn]: null })
234
- }
235
- });
236
- }
237
- }
238
- }
239
-
240
- /**
241
- * Process M2M relationships for an entity
242
- * @private
243
- */
244
- _processM2MRelationships(tableName, config, semantics) {
245
- const manyToMany = config.manyToMany || {};
246
-
247
- for (const [relationKey, m2mConfig] of Object.entries(manyToMany)) {
248
- const { junction_table, local_key, foreign_key, target_entity } = m2mConfig;
249
-
250
- if (!junction_table || !local_key || !foreign_key || !target_entity) {
251
- continue;
252
- }
253
-
254
- const isSelfReferential = target_entity === tableName;
255
- const visual = this._inferVisual('junction', target_entity, isSelfReferential);
256
-
257
- // 1. Source (tableName) can be dropped onto target
258
- if (!semantics[tableName].droppable_on[target_entity]) {
259
- semantics[tableName].droppable_on[target_entity] = [];
260
- }
261
-
262
- const baseEntry = {
263
- relation: junction_table,
264
- type: 'junction',
265
- action: 'link',
266
- visual: visual,
267
- label: this._generateLabel(junction_table, 'link'),
268
- operation: {
269
- method: 'save',
270
- entity: junction_table,
271
- params: {
272
- [local_key]: '@source.id',
273
- [foreign_key]: '@target.id'
274
- }
275
- },
276
- removable: true,
277
- remove_operation: {
278
- method: 'delete',
279
- entity: junction_table,
280
- params: {
281
- [local_key]: '@source.id',
282
- [foreign_key]: '@target.id'
283
- }
284
- },
285
- ...(isSelfReferential && { self_referential: true })
286
- };
287
-
288
- // For edge visual (self-referential), add direction hint
289
- if (visual === 'edge') {
290
- baseEntry.direction = 'source_to_target';
291
- }
292
-
293
- semantics[tableName].droppable_on[target_entity].push(baseEntry);
294
-
295
- // 2. Source (tableName) accepts target being dropped on it
296
- // For M2M, the operation is symmetric but params swap
297
- if (!isSelfReferential && this.entities[target_entity]) {
298
- if (!semantics[tableName].accepts[target_entity]) {
299
- semantics[tableName].accepts[target_entity] = [];
300
- }
301
-
302
- // For accepts on M2M, use frame if target is a set, otherwise badge
303
- const acceptVisual = this._matchesSetPattern(target_entity) ? 'frame' : 'badge';
304
-
305
- semantics[tableName].accepts[target_entity].push({
306
- relation: junction_table,
307
- type: 'junction',
308
- action: 'link',
309
- visual: acceptVisual,
310
- label: this._generateLabel(junction_table, 'link'),
311
- operation: {
312
- method: 'save',
313
- entity: junction_table,
314
- params: {
315
- [local_key]: '@target.id', // target is the entity with the M2M config
316
- [foreign_key]: '@source.id' // source is what's being dropped
317
- }
318
- },
319
- removable: true,
320
- remove_operation: {
321
- method: 'delete',
322
- entity: junction_table,
323
- params: {
324
- [local_key]: '@target.id',
325
- [foreign_key]: '@source.id'
326
- }
327
- }
328
- });
329
- }
330
- }
331
- }
332
-
333
- /**
334
- * Build params object with primary key fields
335
- * @private
336
- */
337
- _buildPKParams(primaryKey, refPrefix, additionalParams) {
338
- const params = {};
339
-
340
- for (const pkField of primaryKey) {
341
- params[pkField] = `${refPrefix}.${pkField}`;
342
- }
343
-
344
- Object.assign(params, additionalParams);
345
- return params;
346
- }
347
-
348
- /**
349
- * Infer the visual representation type for a relationship
350
- * @private
351
- * @param {string} type - 'fk' or 'junction'
352
- * @param {string} targetTable - The target entity name
353
- * @param {boolean} isSelfReferential - Whether this is a self-referential relation
354
- * @returns {string} Visual type: 'containment', 'frame', 'edge', or 'badge'
355
- */
356
- _inferVisual(type, targetTable, isSelfReferential) {
357
- // Rule 1: Self-referential junction → edge (arrows between same entity type)
358
- if (type === 'junction' && isSelfReferential) {
359
- return 'edge';
360
- }
361
-
362
- // Rule 2: Self-referential FK → containment (tree/hierarchy)
363
- if (type === 'fk' && isSelfReferential) {
364
- return 'containment';
365
- }
366
-
367
- // Rule 3: Target entity has self-referential FK → it's a tree/container
368
- if (this._isTreeEntity(targetTable)) {
369
- return 'containment';
370
- }
371
-
372
- // Rule 4: Naming convention fallback
373
- if (this._matchesContainerPattern(targetTable)) {
374
- return 'containment';
375
- }
376
-
377
- if (this._matchesSetPattern(targetTable)) {
378
- return 'frame';
379
- }
380
-
381
- // Default: badge (tag/chip on node)
382
- return 'badge';
383
- }
384
-
385
- /**
386
- * Check if an entity has a self-referential FK (making it a tree structure)
387
- * @private
388
- */
389
- _isTreeEntity(tableName) {
390
- const config = this.entities[tableName];
391
- if (!config) return false;
392
-
393
- const fkIncludes = config.fkIncludes || {};
394
- for (const [alias, target] of Object.entries(fkIncludes)) {
395
- if (target === tableName) {
396
- return true;
397
- }
398
- }
399
- return false;
400
- }
401
-
402
- /**
403
- * Check if entity name matches container patterns
404
- * @private
405
- */
406
- _matchesContainerPattern(tableName) {
407
- const patterns = ['_groups', '_folders', '_categories', '_containers', '_parents'];
408
- return patterns.some(p => tableName.endsWith(p));
409
- }
410
-
411
- /**
412
- * Check if entity name matches set/collection patterns
413
- * @private
414
- */
415
- _matchesSetPattern(tableName) {
416
- const patterns = ['_sets', '_collections', '_lists', '_pools', '_batches'];
417
- return patterns.some(p => tableName.endsWith(p));
418
- }
419
-
420
- /**
421
- * Determine action type for self-referential FK
422
- * @private
423
- */
424
- _getSelfReferentialAction(fkColumn) {
425
- if (fkColumn.includes('parent')) {
426
- return 'reparent';
427
- }
428
- if (fkColumn.includes('depends') || fkColumn.includes('dependency')) {
429
- return 'link';
430
- }
431
- return 'nest';
432
- }
433
-
434
- /**
435
- * Check if an FK alias represents an "assign" pattern
436
- * where the natural gesture is to drop the target onto the source
437
- * e.g., "assigned_to_user" - you drop user onto task, not task onto user
438
- * @private
439
- */
440
- _isAssignPattern(alias) {
441
- const assignPatterns = [
442
- /^assigned_to_/,
443
- /^created_by_/,
444
- /^updated_by_/,
445
- /^owned_by_/,
446
- /^approved_by_/,
447
- /^reviewed_by_/,
448
- /^managed_by_/,
449
- /^author$/,
450
- /^owner$/,
451
- /^assignee$/,
452
- /^reviewer$/,
453
- /^approver$/
454
- ];
455
-
456
- return assignPatterns.some(pattern => pattern.test(alias));
457
- }
458
-
459
- /**
460
- * Generate human-readable label from relation name
461
- * @private
462
- */
463
- _generateLabel(relationName, action) {
464
- // Remove common suffixes and extract the core noun
465
- let name = relationName
466
- .replace(/_id$/, '')
467
- .replace(/^fk_/, '');
468
-
469
- // Strip preposition patterns to get the core entity name
470
- // "assigned_to_user" → "user"
471
- // "depends_on_task" → "task" (but keep "depends on" for special handling)
472
- // "created_by_user" → "user"
473
- // "owner_org" → "org"
474
- const prepositionPatterns = [
475
- /^assigned_to_/,
476
- /^created_by_/,
477
- /^updated_by_/,
478
- /^owned_by_/,
479
- /^belongs_to_/,
480
- /^managed_by_/,
481
- /^approved_by_/,
482
- /^reviewed_by_/
483
- ];
484
-
485
- let strippedName = name;
486
- for (const pattern of prepositionPatterns) {
487
- if (pattern.test(name)) {
488
- strippedName = name.replace(pattern, '');
489
- break;
490
- }
491
- }
492
-
493
- // Convert snake_case to Title Case
494
- const words = strippedName.split('_').map(word =>
495
- word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
496
- );
497
-
498
- // Map action to verb
499
- const verbs = {
500
- 'move': 'Move to',
501
- 'assign': 'Assign',
502
- 'link': 'Add',
503
- 'nest': 'Set as child of',
504
- 'reparent': 'Set parent'
505
- };
506
-
507
- const verb = verbs[action] || '';
508
-
509
- // Special handling for junction tables (link action)
510
- if (action === 'link') {
511
- const singularName = this._singularize(words.join(' '));
512
- return `Add ${singularName.toLowerCase()}`;
513
- }
514
-
515
- return `${verb} ${words.join(' ').toLowerCase()}`.trim();
516
- }
517
-
518
- /**
519
- * Simple singularization
520
- * @private
521
- */
522
- _singularize(word) {
523
- if (word.endsWith('ies')) {
524
- return word.slice(0, -3) + 'y';
525
- }
526
- if (word.endsWith('es') && !word.endsWith('ses')) {
527
- return word.slice(0, -2);
528
- }
529
- if (word.endsWith('s') && !word.endsWith('ss')) {
530
- return word.slice(0, -1);
531
- }
532
- return word;
533
- }
534
- }
535
-
536
- /**
537
- * Generate drop semantics from parsed entities
538
- * @param {Array|Object} entities - Array of entity configs or map of tableName -> config
539
- * @returns {Object} Drop semantics manifest
540
- */
541
- export function generateDropSemantics(entities) {
542
- // Convert array to map if needed
543
- let entityMap = entities;
544
- if (Array.isArray(entities)) {
545
- entityMap = {};
546
- for (const entity of entities) {
547
- entityMap[entity.tableName] = entity;
548
- }
549
- }
550
-
551
- const gen = new DropSemanticsCodegen(entityMap);
552
- return gen.generate();
553
- }