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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- 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
|
-
}
|