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.
- 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 +309 -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 +653 -0
- package/docs/project-setup.md +456 -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 +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -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,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission/Notification Path Parser
|
|
3
|
-
* Parses DZQL path DSL into AST for code generation
|
|
4
|
-
*
|
|
5
|
-
* Path Grammar:
|
|
6
|
-
* - Direct field: @field_name
|
|
7
|
-
* - FK traversal: @field->table.target_field
|
|
8
|
-
* - Conditional: @field->table[condition]{temporal}.target_field
|
|
9
|
-
* - Complex: field1.field2->table[filter].target
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export class PathParser {
|
|
13
|
-
/**
|
|
14
|
-
* Parse a permission/notification path into an AST
|
|
15
|
-
* @param {string} path - Path string to parse
|
|
16
|
-
* @returns {Object} AST representation
|
|
17
|
-
*/
|
|
18
|
-
parse(path) {
|
|
19
|
-
if (!path || path.trim() === '') {
|
|
20
|
-
return { type: 'empty' };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Direct field reference: @owner_id
|
|
24
|
-
if (path.match(/^@\w+$/)) {
|
|
25
|
-
return {
|
|
26
|
-
type: 'direct_field',
|
|
27
|
-
field: path.substring(1)
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Complex path with traversal
|
|
32
|
-
if (path.includes('->')) {
|
|
33
|
-
return this._parseTraversalPath(path);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Field path with dot notation: field1.field2
|
|
37
|
-
if (path.includes('.') && !path.includes('->')) {
|
|
38
|
-
return this._parseDotPath(path);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Unknown format
|
|
42
|
-
return {
|
|
43
|
-
type: 'unknown',
|
|
44
|
-
path
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Parse a traversal path (contains ->)
|
|
50
|
-
* @private
|
|
51
|
-
*/
|
|
52
|
-
_parseTraversalPath(path) {
|
|
53
|
-
const steps = [];
|
|
54
|
-
const parts = path.split('->');
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < parts.length; i++) {
|
|
57
|
-
const part = parts[i].trim();
|
|
58
|
-
|
|
59
|
-
if (i === 0) {
|
|
60
|
-
// First part: source field(s)
|
|
61
|
-
steps.push(this._parseSourceField(part));
|
|
62
|
-
} else if (i === parts.length - 1) {
|
|
63
|
-
// Last part: target field
|
|
64
|
-
steps.push(this._parseTargetField(part));
|
|
65
|
-
} else {
|
|
66
|
-
// Middle part: table with optional condition
|
|
67
|
-
steps.push(this._parseTableReference(part));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
type: 'traversal',
|
|
73
|
-
steps
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Parse source field (can be @field or field.subfield)
|
|
79
|
-
* @private
|
|
80
|
-
*/
|
|
81
|
-
_parseSourceField(part) {
|
|
82
|
-
if (part.startsWith('@')) {
|
|
83
|
-
return {
|
|
84
|
-
type: 'field_ref',
|
|
85
|
-
field: part.substring(1)
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (part.includes('.')) {
|
|
90
|
-
const fields = part.split('.');
|
|
91
|
-
return {
|
|
92
|
-
type: 'dot_path',
|
|
93
|
-
fields
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
type: 'field_ref',
|
|
99
|
-
field: part
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Parse table reference with optional filter and temporal marker
|
|
105
|
-
* Example: acts_for[org_id=$,role='admin']{active}
|
|
106
|
-
* @private
|
|
107
|
-
*/
|
|
108
|
-
_parseTableReference(part) {
|
|
109
|
-
const result = {
|
|
110
|
-
type: 'table_ref',
|
|
111
|
-
table: null,
|
|
112
|
-
filter: null,
|
|
113
|
-
temporal: false,
|
|
114
|
-
targetField: null
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Extract temporal marker {active}
|
|
118
|
-
if (part.includes('{active}')) {
|
|
119
|
-
result.temporal = true;
|
|
120
|
-
part = part.replace('{active}', '');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Extract filter [condition]
|
|
124
|
-
const filterMatch = part.match(/([a-z_]+)\[(.*?)\](\.(.+))?/i);
|
|
125
|
-
if (filterMatch) {
|
|
126
|
-
result.table = filterMatch[1];
|
|
127
|
-
result.filter = this._parseFilter(filterMatch[2]);
|
|
128
|
-
result.targetField = filterMatch[4] || null;
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Simple table.field reference
|
|
133
|
-
const dotMatch = part.match(/([a-z_]+)\.(.+)/i);
|
|
134
|
-
if (dotMatch) {
|
|
135
|
-
result.table = dotMatch[1];
|
|
136
|
-
result.targetField = dotMatch[2];
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Just table name
|
|
141
|
-
result.table = part;
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Parse filter conditions
|
|
147
|
-
* Example: org_id=$,role='admin'
|
|
148
|
-
* @private
|
|
149
|
-
*/
|
|
150
|
-
_parseFilter(filterStr) {
|
|
151
|
-
const conditions = [];
|
|
152
|
-
const parts = filterStr.split(',');
|
|
153
|
-
|
|
154
|
-
for (const part of parts) {
|
|
155
|
-
const trimmed = part.trim();
|
|
156
|
-
|
|
157
|
-
// Handle various comparison operators
|
|
158
|
-
if (trimmed.includes('=')) {
|
|
159
|
-
const [field, value] = trimmed.split('=').map(s => s.trim());
|
|
160
|
-
conditions.push({
|
|
161
|
-
field,
|
|
162
|
-
operator: '=',
|
|
163
|
-
value: value === '$' ? { type: 'param' } : this._parseValue(value)
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return conditions;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Parse a value (string literal, number, param reference)
|
|
173
|
-
* @private
|
|
174
|
-
*/
|
|
175
|
-
_parseValue(value) {
|
|
176
|
-
// String literal
|
|
177
|
-
if (value.startsWith("'") && value.endsWith("'")) {
|
|
178
|
-
return {
|
|
179
|
-
type: 'literal',
|
|
180
|
-
value: value.slice(1, -1)
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Number
|
|
185
|
-
if (/^\d+$/.test(value)) {
|
|
186
|
-
return {
|
|
187
|
-
type: 'number',
|
|
188
|
-
value: parseInt(value)
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Field reference
|
|
193
|
-
if (value.startsWith('@')) {
|
|
194
|
-
return {
|
|
195
|
-
type: 'field',
|
|
196
|
-
value: value.substring(1)
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
type: 'literal',
|
|
202
|
-
value
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Parse target field (last part of path)
|
|
208
|
-
* Example: user_id or users.user_id
|
|
209
|
-
* @private
|
|
210
|
-
*/
|
|
211
|
-
_parseTargetField(part) {
|
|
212
|
-
// Check for temporal marker before removing it
|
|
213
|
-
const hasTemporal = part.includes('{active}');
|
|
214
|
-
|
|
215
|
-
// Remove temporal marker if present
|
|
216
|
-
part = part.replace('{active}', '');
|
|
217
|
-
|
|
218
|
-
// Check for table[filter].field pattern
|
|
219
|
-
const filterMatch = part.match(/([a-z_]+)\[(.*?)\]\.(.+)/i);
|
|
220
|
-
if (filterMatch) {
|
|
221
|
-
return {
|
|
222
|
-
type: 'table_ref',
|
|
223
|
-
table: filterMatch[1],
|
|
224
|
-
filter: this._parseFilter(filterMatch[2]),
|
|
225
|
-
temporal: hasTemporal,
|
|
226
|
-
targetField: filterMatch[3]
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Simple field reference
|
|
231
|
-
if (!part.includes('.')) {
|
|
232
|
-
return {
|
|
233
|
-
type: 'field_ref',
|
|
234
|
-
field: part
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Dot notation
|
|
239
|
-
const fields = part.split('.');
|
|
240
|
-
return {
|
|
241
|
-
type: 'dot_path',
|
|
242
|
-
fields
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Parse dot path (field.subfield.subsubfield)
|
|
248
|
-
* @private
|
|
249
|
-
*/
|
|
250
|
-
_parseDotPath(path) {
|
|
251
|
-
const fields = path.split('.').map(f => f.trim());
|
|
252
|
-
return {
|
|
253
|
-
type: 'dot_path',
|
|
254
|
-
fields
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Parse multiple paths (used in permission arrays)
|
|
260
|
-
* @param {Array<string>} paths - Array of path strings
|
|
261
|
-
* @returns {Array<Object>} Array of ASTs
|
|
262
|
-
*/
|
|
263
|
-
parseMultiple(paths) {
|
|
264
|
-
if (!Array.isArray(paths)) {
|
|
265
|
-
return [];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return paths.map(path => this.parse(path));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Utility function to parse a single path
|
|
274
|
-
* @param {string} path - Path to parse
|
|
275
|
-
* @returns {Object} AST
|
|
276
|
-
*/
|
|
277
|
-
export function parsePath(path) {
|
|
278
|
-
const parser = new PathParser();
|
|
279
|
-
return parser.parse(path);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Utility function to parse multiple paths
|
|
284
|
-
* @param {Array<string>} paths - Paths to parse
|
|
285
|
-
* @returns {Array<Object>} ASTs
|
|
286
|
-
*/
|
|
287
|
-
export function parsePaths(paths) {
|
|
288
|
-
const parser = new PathParser();
|
|
289
|
-
return parser.parseMultiple(paths);
|
|
290
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscribable Definition Parser
|
|
3
|
-
* Parses register_subscribable() calls and extracts configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export class SubscribableParser {
|
|
7
|
-
/**
|
|
8
|
-
* Parse a dzql.register_subscribable() call from SQL
|
|
9
|
-
* @param {string} sql - SQL containing register_subscribable call
|
|
10
|
-
* @returns {Object} Parsed subscribable configuration
|
|
11
|
-
*/
|
|
12
|
-
parseFromSQL(sql) {
|
|
13
|
-
// Extract the register_subscribable call
|
|
14
|
-
const registerMatch = sql.match(/dzql\.register_subscribable\s*\(([\s\S]*?)\);/i);
|
|
15
|
-
if (!registerMatch) {
|
|
16
|
-
throw new Error('No register_subscribable call found in SQL');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const params = this._parseParameters(registerMatch[1]);
|
|
20
|
-
|
|
21
|
-
return this._buildSubscribableConfig(params);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Parse parameters from register_subscribable call
|
|
26
|
-
* @private
|
|
27
|
-
*/
|
|
28
|
-
_parseParameters(paramsString) {
|
|
29
|
-
// Split by commas that are not inside quotes, parentheses, or brackets
|
|
30
|
-
const params = [];
|
|
31
|
-
let currentParam = '';
|
|
32
|
-
let depth = 0;
|
|
33
|
-
let inString = false;
|
|
34
|
-
let stringChar = null;
|
|
35
|
-
|
|
36
|
-
for (let i = 0; i < paramsString.length; i++) {
|
|
37
|
-
const char = paramsString[i];
|
|
38
|
-
const prevChar = i > 0 ? paramsString[i - 1] : '';
|
|
39
|
-
|
|
40
|
-
if ((char === "'" || char === '"') && prevChar !== '\\') {
|
|
41
|
-
if (!inString) {
|
|
42
|
-
inString = true;
|
|
43
|
-
stringChar = char;
|
|
44
|
-
} else if (char === stringChar) {
|
|
45
|
-
inString = false;
|
|
46
|
-
stringChar = null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!inString) {
|
|
51
|
-
if (char === '(' || char === '{' || char === '[') depth++;
|
|
52
|
-
if (char === ')' || char === '}' || char === ']') depth--;
|
|
53
|
-
|
|
54
|
-
if (char === ',' && depth === 0) {
|
|
55
|
-
params.push(currentParam.trim());
|
|
56
|
-
currentParam = '';
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
currentParam += char;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (currentParam.trim()) {
|
|
65
|
-
params.push(currentParam.trim());
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return params;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Build subscribable configuration from parsed parameters
|
|
73
|
-
* register_subscribable(name, permission_paths, param_schema, root_entity, relations)
|
|
74
|
-
* @private
|
|
75
|
-
*/
|
|
76
|
-
_buildSubscribableConfig(params) {
|
|
77
|
-
const config = {
|
|
78
|
-
name: this._cleanString(params[0]),
|
|
79
|
-
permissionPaths: params[1] ? this._parseJSON(params[1]) : {},
|
|
80
|
-
paramSchema: params[2] ? this._parseJSON(params[2]) : {},
|
|
81
|
-
rootEntity: this._cleanString(params[3]),
|
|
82
|
-
relations: params[4] ? this._parseJSON(params[4]) : {}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return config;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Parse from JavaScript object (for programmatic usage)
|
|
90
|
-
* @param {Object} obj - Subscribable configuration object
|
|
91
|
-
* @returns {Object} Normalized configuration
|
|
92
|
-
*/
|
|
93
|
-
parseFromObject(obj) {
|
|
94
|
-
return {
|
|
95
|
-
name: obj.name,
|
|
96
|
-
permissionPaths: obj.permissionPaths || obj.permissions || {},
|
|
97
|
-
paramSchema: obj.paramSchema || obj.params || {},
|
|
98
|
-
rootEntity: obj.rootEntity || obj.root,
|
|
99
|
-
relations: obj.relations || {}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Parse multiple subscribables from SQL file
|
|
105
|
-
* @param {string} sql - SQL file content
|
|
106
|
-
* @returns {Array} Array of subscribable configurations
|
|
107
|
-
*/
|
|
108
|
-
parseAllFromSQL(sql) {
|
|
109
|
-
const subscribables = [];
|
|
110
|
-
const regex = /dzql\.register_subscribable\s*\(([\s\S]*?)\);/gi;
|
|
111
|
-
let match;
|
|
112
|
-
|
|
113
|
-
while ((match = regex.exec(sql)) !== null) {
|
|
114
|
-
try {
|
|
115
|
-
const params = this._parseParameters(match[1]);
|
|
116
|
-
const config = this._buildSubscribableConfig(params);
|
|
117
|
-
subscribables.push(config);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error('Failed to parse subscribable:', error.message);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return subscribables;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Clean a string parameter (remove quotes)
|
|
128
|
-
* @private
|
|
129
|
-
*/
|
|
130
|
-
_cleanString(str) {
|
|
131
|
-
if (!str) return '';
|
|
132
|
-
// Handle SQL NULL keyword - return empty string for null values
|
|
133
|
-
if (str.trim().toUpperCase() === 'NULL') return '';
|
|
134
|
-
// Remove outer quotes, SQL comments, then any remaining quotes and whitespace
|
|
135
|
-
let cleaned = str.replace(/^['"]|['"]$/g, ''); // Remove outer quotes
|
|
136
|
-
cleaned = cleaned.replace(/--[^\n]*/g, ''); // Remove SQL comments
|
|
137
|
-
cleaned = cleaned.replace(/['"\s]+$/g, ''); // Remove trailing quotes/whitespace
|
|
138
|
-
return cleaned.trim();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Parse JSONB object parameter
|
|
143
|
-
* @private
|
|
144
|
-
*/
|
|
145
|
-
_parseJSON(str) {
|
|
146
|
-
if (!str || str === '{}' || str === "'{}'::jsonb") {
|
|
147
|
-
return {};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Handle jsonb_build_object() syntax
|
|
151
|
-
if (str.includes('jsonb_build_object')) {
|
|
152
|
-
return this._parseJSONBuildObject(str);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Handle plain JSON string
|
|
156
|
-
try {
|
|
157
|
-
// Remove ::jsonb cast
|
|
158
|
-
let cleaned = str.replace(/::jsonb$/i, '');
|
|
159
|
-
// Remove outer quotes if it's a string literal (handles multi-line with [\s\S])
|
|
160
|
-
cleaned = cleaned.replace(/^'([\s\S]*)'$/, '$1');
|
|
161
|
-
// Unescape internal quotes
|
|
162
|
-
cleaned = cleaned.replace(/''/g, "'");
|
|
163
|
-
|
|
164
|
-
return JSON.parse(cleaned);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error('Failed to parse JSON:', str, error);
|
|
167
|
-
return {};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Parse jsonb_build_object() calls
|
|
173
|
-
* @private
|
|
174
|
-
*/
|
|
175
|
-
_parseJSONBuildObject(str) {
|
|
176
|
-
const result = {};
|
|
177
|
-
|
|
178
|
-
// Extract content between jsonb_build_object( and )
|
|
179
|
-
const match = str.match(/jsonb_build_object\s*\(([\s\S]*)\)/i);
|
|
180
|
-
if (!match) return result;
|
|
181
|
-
|
|
182
|
-
// Parse key-value pairs
|
|
183
|
-
const params = this._parseParameters(match[1]);
|
|
184
|
-
|
|
185
|
-
for (let i = 0; i < params.length; i += 2) {
|
|
186
|
-
if (i + 1 < params.length) {
|
|
187
|
-
const key = this._cleanString(params[i]);
|
|
188
|
-
let value = params[i + 1];
|
|
189
|
-
|
|
190
|
-
// Check if value is nested jsonb_build_object or array
|
|
191
|
-
if (value.includes('jsonb_build_object')) {
|
|
192
|
-
value = this._parseJSONBuildObject(value);
|
|
193
|
-
} else if (value.includes('jsonb_build_array')) {
|
|
194
|
-
value = this._parseJSONBArray(value);
|
|
195
|
-
} else if (value.includes('ARRAY[')) {
|
|
196
|
-
value = this._parseArray(value);
|
|
197
|
-
} else {
|
|
198
|
-
value = this._cleanString(value);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
result[key] = value;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Parse jsonb_build_array() calls
|
|
210
|
-
* @private
|
|
211
|
-
*/
|
|
212
|
-
_parseJSONBArray(str) {
|
|
213
|
-
const match = str.match(/jsonb_build_array\s*\(([\s\S]*)\)/i);
|
|
214
|
-
if (!match) return [];
|
|
215
|
-
|
|
216
|
-
const params = this._parseParameters(match[1]);
|
|
217
|
-
return params.map(p => {
|
|
218
|
-
if (p.includes('jsonb_build_object')) {
|
|
219
|
-
return this._parseJSONBuildObject(p);
|
|
220
|
-
}
|
|
221
|
-
return this._cleanString(p);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Parse ARRAY[...] syntax
|
|
227
|
-
* @private
|
|
228
|
-
*/
|
|
229
|
-
_parseArray(str) {
|
|
230
|
-
if (!str || str === 'ARRAY[]::text[]') {
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Extract content between ARRAY[ and ]
|
|
235
|
-
const match = str.match(/ARRAY\[(.*?)\]/i);
|
|
236
|
-
if (!match) return [];
|
|
237
|
-
|
|
238
|
-
// Split by comma and clean each element
|
|
239
|
-
return match[1]
|
|
240
|
-
.split(',')
|
|
241
|
-
.map(s => this._cleanString(s))
|
|
242
|
-
.filter(s => s.length > 0);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- DZQL Core - Minimal Foundation
|
|
3
|
-
-- ============================================================================
|
|
4
|
-
-- This is the minimal SQL needed for DZQL compiled mode.
|
|
5
|
-
-- Run: dzql db:init
|
|
6
|
-
-- Or: psql $DATABASE_URL -f dzql-core.sql
|
|
7
|
-
-- ============================================================================
|
|
8
|
-
|
|
9
|
-
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
10
|
-
|
|
11
|
-
CREATE SCHEMA IF NOT EXISTS dzql;
|
|
12
|
-
|
|
13
|
-
-- Version tracking
|
|
14
|
-
CREATE TABLE IF NOT EXISTS dzql.meta (
|
|
15
|
-
installed_at TIMESTAMPTZ DEFAULT now(),
|
|
16
|
-
version TEXT NOT NULL
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
INSERT INTO dzql.meta (version)
|
|
20
|
-
SELECT '0.5.14'
|
|
21
|
-
WHERE NOT EXISTS (SELECT 1 FROM dzql.meta);
|
|
22
|
-
|
|
23
|
-
-- Entity registry
|
|
24
|
-
CREATE TABLE IF NOT EXISTS dzql.entities (
|
|
25
|
-
table_name TEXT PRIMARY KEY,
|
|
26
|
-
label_field TEXT NOT NULL,
|
|
27
|
-
searchable_fields TEXT[] NOT NULL,
|
|
28
|
-
fk_includes JSONB DEFAULT '{}',
|
|
29
|
-
soft_delete BOOLEAN DEFAULT false,
|
|
30
|
-
temporal_fields JSONB DEFAULT '{}',
|
|
31
|
-
notification_paths JSONB DEFAULT '{}',
|
|
32
|
-
permission_paths JSONB DEFAULT '{}',
|
|
33
|
-
graph_rules JSONB DEFAULT '{}',
|
|
34
|
-
field_defaults JSONB DEFAULT '{}',
|
|
35
|
-
many_to_many JSONB DEFAULT '{}'
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
-- Function allowlist
|
|
39
|
-
CREATE TABLE IF NOT EXISTS dzql.registry (
|
|
40
|
-
fn_regproc REGPROC PRIMARY KEY,
|
|
41
|
-
description TEXT
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
-- Event audit table
|
|
45
|
-
CREATE TABLE IF NOT EXISTS dzql.events (
|
|
46
|
-
event_id BIGSERIAL PRIMARY KEY,
|
|
47
|
-
table_name TEXT NOT NULL,
|
|
48
|
-
op TEXT NOT NULL,
|
|
49
|
-
pk JSONB NOT NULL,
|
|
50
|
-
data JSONB,
|
|
51
|
-
user_id INT,
|
|
52
|
-
notify_users INT[],
|
|
53
|
-
at TIMESTAMPTZ DEFAULT now()
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
CREATE INDEX IF NOT EXISTS dzql_events_at_idx ON dzql.events (at);
|
|
57
|
-
CREATE INDEX IF NOT EXISTS dzql_events_table_pk_idx ON dzql.events (table_name, pk, at);
|
|
58
|
-
|
|
59
|
-
-- NOTIFY trigger for real-time updates
|
|
60
|
-
CREATE OR REPLACE FUNCTION dzql.notify_event()
|
|
61
|
-
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
62
|
-
BEGIN
|
|
63
|
-
PERFORM pg_notify('dzql', jsonb_build_object(
|
|
64
|
-
'event_id', NEW.event_id,
|
|
65
|
-
'table', NEW.table_name,
|
|
66
|
-
'op', NEW.op,
|
|
67
|
-
'pk', NEW.pk,
|
|
68
|
-
'data', NEW.data,
|
|
69
|
-
'user_id', NEW.user_id,
|
|
70
|
-
'at', NEW.at,
|
|
71
|
-
'notify_users', NEW.notify_users
|
|
72
|
-
)::text);
|
|
73
|
-
RETURN NULL;
|
|
74
|
-
END $$;
|
|
75
|
-
|
|
76
|
-
DROP TRIGGER IF EXISTS dzql_events_notify ON dzql.events;
|
|
77
|
-
CREATE TRIGGER dzql_events_notify
|
|
78
|
-
AFTER INSERT ON dzql.events
|
|
79
|
-
FOR EACH ROW EXECUTE FUNCTION dzql.notify_event();
|
|
80
|
-
|
|
81
|
-
-- Subscribables registry (for compiled subscribables)
|
|
82
|
-
CREATE TABLE IF NOT EXISTS dzql.subscribables (
|
|
83
|
-
name TEXT PRIMARY KEY,
|
|
84
|
-
permission_paths JSONB DEFAULT '{}',
|
|
85
|
-
param_schema JSONB DEFAULT '{}',
|
|
86
|
-
root_entity TEXT NOT NULL,
|
|
87
|
-
relations JSONB DEFAULT '{}',
|
|
88
|
-
scope_tables TEXT[] DEFAULT '{}',
|
|
89
|
-
created_at TIMESTAMPTZ DEFAULT now()
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
-- Helper to register entities
|
|
93
|
-
CREATE OR REPLACE FUNCTION dzql.register_entity(
|
|
94
|
-
p_table_name TEXT,
|
|
95
|
-
p_label_field TEXT,
|
|
96
|
-
p_searchable_fields TEXT[],
|
|
97
|
-
p_fk_includes JSONB DEFAULT '{}',
|
|
98
|
-
p_soft_delete BOOLEAN DEFAULT false,
|
|
99
|
-
p_temporal_fields JSONB DEFAULT '{}',
|
|
100
|
-
p_notification_paths JSONB DEFAULT '{}',
|
|
101
|
-
p_permission_paths JSONB DEFAULT '{}',
|
|
102
|
-
p_graph_rules JSONB DEFAULT '{}',
|
|
103
|
-
p_field_defaults JSONB DEFAULT '{}',
|
|
104
|
-
p_many_to_many JSONB DEFAULT '{}'
|
|
105
|
-
) RETURNS VOID AS $$
|
|
106
|
-
BEGIN
|
|
107
|
-
INSERT INTO dzql.entities (
|
|
108
|
-
table_name, label_field, searchable_fields, fk_includes,
|
|
109
|
-
soft_delete, temporal_fields, notification_paths, permission_paths,
|
|
110
|
-
graph_rules, field_defaults, many_to_many
|
|
111
|
-
) VALUES (
|
|
112
|
-
p_table_name, p_label_field, p_searchable_fields, p_fk_includes,
|
|
113
|
-
p_soft_delete, p_temporal_fields, p_notification_paths, p_permission_paths,
|
|
114
|
-
p_graph_rules, p_field_defaults, p_many_to_many
|
|
115
|
-
)
|
|
116
|
-
ON CONFLICT (table_name) DO UPDATE SET
|
|
117
|
-
label_field = EXCLUDED.label_field,
|
|
118
|
-
searchable_fields = EXCLUDED.searchable_fields,
|
|
119
|
-
fk_includes = EXCLUDED.fk_includes,
|
|
120
|
-
soft_delete = EXCLUDED.soft_delete,
|
|
121
|
-
temporal_fields = EXCLUDED.temporal_fields,
|
|
122
|
-
notification_paths = EXCLUDED.notification_paths,
|
|
123
|
-
permission_paths = EXCLUDED.permission_paths,
|
|
124
|
-
graph_rules = EXCLUDED.graph_rules,
|
|
125
|
-
field_defaults = EXCLUDED.field_defaults,
|
|
126
|
-
many_to_many = EXCLUDED.many_to_many;
|
|
127
|
-
END;
|
|
128
|
-
$$ LANGUAGE plpgsql;
|
|
129
|
-
|
|
130
|
-
-- Helper to register subscribables
|
|
131
|
-
CREATE OR REPLACE FUNCTION dzql.register_subscribable(
|
|
132
|
-
p_name TEXT,
|
|
133
|
-
p_permission_paths JSONB,
|
|
134
|
-
p_param_schema JSONB,
|
|
135
|
-
p_root_entity TEXT,
|
|
136
|
-
p_relations JSONB DEFAULT '{}'
|
|
137
|
-
) RETURNS VOID AS $$
|
|
138
|
-
DECLARE
|
|
139
|
-
v_scope_tables TEXT[];
|
|
140
|
-
BEGIN
|
|
141
|
-
-- Extract scope tables from relations
|
|
142
|
-
SELECT array_agg(DISTINCT tbl) INTO v_scope_tables
|
|
143
|
-
FROM (
|
|
144
|
-
SELECT p_root_entity AS tbl
|
|
145
|
-
UNION ALL
|
|
146
|
-
SELECT value->>'entity' AS tbl
|
|
147
|
-
FROM jsonb_each(p_relations)
|
|
148
|
-
WHERE value->>'entity' IS NOT NULL
|
|
149
|
-
) t
|
|
150
|
-
WHERE tbl IS NOT NULL;
|
|
151
|
-
|
|
152
|
-
INSERT INTO dzql.subscribables (name, permission_paths, param_schema, root_entity, relations, scope_tables)
|
|
153
|
-
VALUES (p_name, p_permission_paths, p_param_schema, p_root_entity, p_relations, v_scope_tables)
|
|
154
|
-
ON CONFLICT (name) DO UPDATE SET
|
|
155
|
-
permission_paths = EXCLUDED.permission_paths,
|
|
156
|
-
param_schema = EXCLUDED.param_schema,
|
|
157
|
-
root_entity = EXCLUDED.root_entity,
|
|
158
|
-
relations = EXCLUDED.relations,
|
|
159
|
-
scope_tables = EXCLUDED.scope_tables;
|
|
160
|
-
END;
|
|
161
|
-
$$ LANGUAGE plpgsql;
|