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,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;