dzql 0.5.33 → 0.6.0

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 (150) 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 +293 -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 +641 -0
  20. package/docs/project-setup.md +432 -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 +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. 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
- }