dzql 0.1.6 → 0.2.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/README.md +8 -1
- package/docs/REFERENCE.md +139 -0
- package/package.json +2 -2
- package/src/client/ws.js +87 -2
- package/src/compiler/codegen/subscribable-codegen.js +396 -0
- package/src/compiler/compiler.js +115 -0
- package/src/compiler/parser/subscribable-parser.js +242 -0
- package/src/database/migrations/009_subscriptions.sql +230 -0
- package/src/server/index.js +90 -1
- package/src/server/subscriptions.js +209 -0
- package/src/server/ws.js +78 -2
package/src/compiler/compiler.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EntityParser } from './parser/entity-parser.js';
|
|
7
|
+
import { SubscribableParser } from './parser/subscribable-parser.js';
|
|
7
8
|
import { generatePermissionFunctions } from './codegen/permission-codegen.js';
|
|
8
9
|
import { generateOperations } from './codegen/operation-codegen.js';
|
|
9
10
|
import { generateNotificationFunction } from './codegen/notification-codegen.js';
|
|
10
11
|
import { generateGraphRuleFunctions } from './codegen/graph-rules-codegen.js';
|
|
12
|
+
import { generateSubscribable } from './codegen/subscribable-codegen.js';
|
|
11
13
|
import crypto from 'crypto';
|
|
12
14
|
|
|
13
15
|
export class DZQLCompiler {
|
|
@@ -18,6 +20,7 @@ export class DZQLCompiler {
|
|
|
18
20
|
...options
|
|
19
21
|
};
|
|
20
22
|
this.parser = new EntityParser();
|
|
23
|
+
this.subscribableParser = new SubscribableParser();
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -79,6 +82,34 @@ export class DZQLCompiler {
|
|
|
79
82
|
return result;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Compile a subscribable definition to SQL
|
|
87
|
+
* @param {Object} subscribable - Subscribable configuration
|
|
88
|
+
* @returns {Object} Compilation result
|
|
89
|
+
*/
|
|
90
|
+
compileSubscribable(subscribable) {
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
|
|
93
|
+
// Normalize subscribable configuration
|
|
94
|
+
const normalized = typeof subscribable.name === 'string'
|
|
95
|
+
? subscribable
|
|
96
|
+
: this.subscribableParser.parseFromObject(subscribable);
|
|
97
|
+
|
|
98
|
+
// Generate SQL
|
|
99
|
+
const sql = generateSubscribable(normalized);
|
|
100
|
+
|
|
101
|
+
// Calculate checksum
|
|
102
|
+
const checksum = this._calculateChecksum(sql);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name: normalized.name,
|
|
106
|
+
sql,
|
|
107
|
+
checksum,
|
|
108
|
+
compilationTime: Date.now() - startTime,
|
|
109
|
+
generatedAt: new Date().toISOString()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
/**
|
|
83
114
|
* Compile multiple entities
|
|
84
115
|
* @param {Array} entities - Array of entity configurations
|
|
@@ -111,6 +142,38 @@ export class DZQLCompiler {
|
|
|
111
142
|
};
|
|
112
143
|
}
|
|
113
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Compile multiple subscribables
|
|
147
|
+
* @param {Array} subscribables - Array of subscribable configurations
|
|
148
|
+
* @returns {Object} Compilation results
|
|
149
|
+
*/
|
|
150
|
+
compileAllSubscribables(subscribables) {
|
|
151
|
+
const results = [];
|
|
152
|
+
const errors = [];
|
|
153
|
+
|
|
154
|
+
for (const subscribable of subscribables) {
|
|
155
|
+
try {
|
|
156
|
+
const result = this.compileSubscribable(subscribable);
|
|
157
|
+
results.push(result);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
errors.push({
|
|
160
|
+
subscribable: subscribable.name || 'unknown',
|
|
161
|
+
error: error.message
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
results,
|
|
168
|
+
errors,
|
|
169
|
+
summary: {
|
|
170
|
+
total: subscribables.length,
|
|
171
|
+
successful: results.length,
|
|
172
|
+
failed: errors.length
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
114
177
|
/**
|
|
115
178
|
* Compile from SQL file
|
|
116
179
|
* @param {string} sqlContent - SQL file content
|
|
@@ -140,6 +203,25 @@ export class DZQLCompiler {
|
|
|
140
203
|
return this.compileAll(entities);
|
|
141
204
|
}
|
|
142
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Compile subscribables from SQL file
|
|
208
|
+
* @param {string} sqlContent - SQL file content
|
|
209
|
+
* @returns {Object} Compilation results
|
|
210
|
+
*/
|
|
211
|
+
compileSubscribablesFromSQL(sqlContent) {
|
|
212
|
+
const subscribables = this.subscribableParser.parseAllFromSQL(sqlContent);
|
|
213
|
+
|
|
214
|
+
if (subscribables.length === 0) {
|
|
215
|
+
return {
|
|
216
|
+
results: [],
|
|
217
|
+
errors: [],
|
|
218
|
+
summary: { total: 0, successful: 0, failed: 0 }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return this.compileAllSubscribables(subscribables);
|
|
223
|
+
}
|
|
224
|
+
|
|
143
225
|
/**
|
|
144
226
|
* Generate file header
|
|
145
227
|
* @private
|
|
@@ -226,3 +308,36 @@ export function compileFromSQL(sqlContent, options = {}) {
|
|
|
226
308
|
const compiler = new DZQLCompiler(options);
|
|
227
309
|
return compiler.compileFromSQL(sqlContent);
|
|
228
310
|
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Compile a single subscribable
|
|
314
|
+
* @param {Object} subscribable - Subscribable configuration
|
|
315
|
+
* @param {Object} options - Compiler options
|
|
316
|
+
* @returns {Object} Compilation result
|
|
317
|
+
*/
|
|
318
|
+
export function compileSubscribable(subscribable, options = {}) {
|
|
319
|
+
const compiler = new DZQLCompiler(options);
|
|
320
|
+
return compiler.compileSubscribable(subscribable);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Compile multiple subscribables
|
|
325
|
+
* @param {Array} subscribables - Array of subscribable configurations
|
|
326
|
+
* @param {Object} options - Compiler options
|
|
327
|
+
* @returns {Object} Compilation results
|
|
328
|
+
*/
|
|
329
|
+
export function compileAllSubscribables(subscribables, options = {}) {
|
|
330
|
+
const compiler = new DZQLCompiler(options);
|
|
331
|
+
return compiler.compileAllSubscribables(subscribables);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Compile subscribables from SQL file content
|
|
336
|
+
* @param {string} sqlContent - SQL file content
|
|
337
|
+
* @param {Object} options - Compiler options
|
|
338
|
+
* @returns {Object} Compilation results
|
|
339
|
+
*/
|
|
340
|
+
export function compileSubscribablesFromSQL(sqlContent, options = {}) {
|
|
341
|
+
const compiler = new DZQLCompiler(options);
|
|
342
|
+
return compiler.compileSubscribablesFromSQL(sqlContent);
|
|
343
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
// Remove outer quotes, SQL comments, then any remaining quotes and whitespace
|
|
133
|
+
let cleaned = str.replace(/^['"]|['"]$/g, ''); // Remove outer quotes
|
|
134
|
+
cleaned = cleaned.replace(/--[^\n]*/g, ''); // Remove SQL comments
|
|
135
|
+
cleaned = cleaned.replace(/['"\s]+$/g, ''); // Remove trailing quotes/whitespace
|
|
136
|
+
return cleaned.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse JSONB object parameter
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
_parseJSON(str) {
|
|
144
|
+
if (!str || str === '{}' || str === "'{}'::jsonb") {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle jsonb_build_object() syntax
|
|
149
|
+
if (str.includes('jsonb_build_object')) {
|
|
150
|
+
return this._parseJSONBuildObject(str);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle plain JSON string
|
|
154
|
+
try {
|
|
155
|
+
// Remove ::jsonb cast
|
|
156
|
+
let cleaned = str.replace(/::jsonb$/i, '');
|
|
157
|
+
// Remove outer quotes if it's a string literal
|
|
158
|
+
cleaned = cleaned.replace(/^'(.*)'$/, '$1');
|
|
159
|
+
// Unescape internal quotes
|
|
160
|
+
cleaned = cleaned.replace(/''/g, "'");
|
|
161
|
+
|
|
162
|
+
return JSON.parse(cleaned);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Failed to parse JSON:', str, error);
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse jsonb_build_object() calls
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
_parseJSONBuildObject(str) {
|
|
174
|
+
const result = {};
|
|
175
|
+
|
|
176
|
+
// Extract content between jsonb_build_object( and )
|
|
177
|
+
const match = str.match(/jsonb_build_object\s*\(([\s\S]*)\)/i);
|
|
178
|
+
if (!match) return result;
|
|
179
|
+
|
|
180
|
+
// Parse key-value pairs
|
|
181
|
+
const params = this._parseParameters(match[1]);
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < params.length; i += 2) {
|
|
184
|
+
if (i + 1 < params.length) {
|
|
185
|
+
const key = this._cleanString(params[i]);
|
|
186
|
+
let value = params[i + 1];
|
|
187
|
+
|
|
188
|
+
// Check if value is nested jsonb_build_object or array
|
|
189
|
+
if (value.includes('jsonb_build_object')) {
|
|
190
|
+
value = this._parseJSONBuildObject(value);
|
|
191
|
+
} else if (value.includes('jsonb_build_array')) {
|
|
192
|
+
value = this._parseJSONBArray(value);
|
|
193
|
+
} else if (value.includes('ARRAY[')) {
|
|
194
|
+
value = this._parseArray(value);
|
|
195
|
+
} else {
|
|
196
|
+
value = this._cleanString(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
result[key] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse jsonb_build_array() calls
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
_parseJSONBArray(str) {
|
|
211
|
+
const match = str.match(/jsonb_build_array\s*\(([\s\S]*)\)/i);
|
|
212
|
+
if (!match) return [];
|
|
213
|
+
|
|
214
|
+
const params = this._parseParameters(match[1]);
|
|
215
|
+
return params.map(p => {
|
|
216
|
+
if (p.includes('jsonb_build_object')) {
|
|
217
|
+
return this._parseJSONBuildObject(p);
|
|
218
|
+
}
|
|
219
|
+
return this._cleanString(p);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parse ARRAY[...] syntax
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
_parseArray(str) {
|
|
228
|
+
if (!str || str === 'ARRAY[]::text[]') {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Extract content between ARRAY[ and ]
|
|
233
|
+
const match = str.match(/ARRAY\[(.*?)\]/i);
|
|
234
|
+
if (!match) return [];
|
|
235
|
+
|
|
236
|
+
// Split by comma and clean each element
|
|
237
|
+
return match[1]
|
|
238
|
+
.split(',')
|
|
239
|
+
.map(s => this._cleanString(s))
|
|
240
|
+
.filter(s => s.length > 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
-- Migration 009: Live Query Subscriptions Infrastructure
|
|
2
|
+
-- Adds support for subscribable documents (Pattern 1 from vision.md)
|
|
3
|
+
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- Subscribables Registry (Metadata Only)
|
|
6
|
+
-- ============================================================================
|
|
7
|
+
|
|
8
|
+
-- Stores metadata for registered subscribables
|
|
9
|
+
-- Note: Active subscriptions are held in-memory on the server
|
|
10
|
+
CREATE TABLE IF NOT EXISTS dzql.subscribables (
|
|
11
|
+
name TEXT PRIMARY KEY,
|
|
12
|
+
permission_paths JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
13
|
+
param_schema JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
14
|
+
root_entity TEXT NOT NULL,
|
|
15
|
+
relations JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
16
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
17
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
COMMENT ON TABLE dzql.subscribables IS
|
|
21
|
+
'Registry of subscribable documents. Subscribables define denormalized views that clients can subscribe to for real-time updates.';
|
|
22
|
+
|
|
23
|
+
COMMENT ON COLUMN dzql.subscribables.name IS
|
|
24
|
+
'Subscribable identifier (used in subscribe_<name> RPC calls)';
|
|
25
|
+
|
|
26
|
+
COMMENT ON COLUMN dzql.subscribables.permission_paths IS
|
|
27
|
+
'Access control paths (e.g., {"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]})';
|
|
28
|
+
|
|
29
|
+
COMMENT ON COLUMN dzql.subscribables.param_schema IS
|
|
30
|
+
'Parameter schema defining subscription key (e.g., {"venue_id": "int"})';
|
|
31
|
+
|
|
32
|
+
COMMENT ON COLUMN dzql.subscribables.root_entity IS
|
|
33
|
+
'Root table for the subscribable document';
|
|
34
|
+
|
|
35
|
+
COMMENT ON COLUMN dzql.subscribables.relations IS
|
|
36
|
+
'Related entities to include (e.g., {"org": "organisations", "sites": {"entity": "sites", "filter": "venue_id=$venue_id"}})';
|
|
37
|
+
|
|
38
|
+
-- Index for quick lookups
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_subscribables_root_entity
|
|
40
|
+
ON dzql.subscribables(root_entity);
|
|
41
|
+
|
|
42
|
+
-- ============================================================================
|
|
43
|
+
-- Register Subscribable Function
|
|
44
|
+
-- ============================================================================
|
|
45
|
+
|
|
46
|
+
CREATE OR REPLACE FUNCTION dzql.register_subscribable(
|
|
47
|
+
p_name TEXT,
|
|
48
|
+
p_permission_paths JSONB,
|
|
49
|
+
p_param_schema JSONB,
|
|
50
|
+
p_root_entity TEXT,
|
|
51
|
+
p_relations JSONB
|
|
52
|
+
) RETURNS TEXT AS $$
|
|
53
|
+
DECLARE
|
|
54
|
+
v_result TEXT;
|
|
55
|
+
BEGIN
|
|
56
|
+
-- Validate inputs
|
|
57
|
+
IF p_name IS NULL OR p_name = '' THEN
|
|
58
|
+
RAISE EXCEPTION 'Subscribable name cannot be empty';
|
|
59
|
+
END IF;
|
|
60
|
+
|
|
61
|
+
IF p_root_entity IS NULL OR p_root_entity = '' THEN
|
|
62
|
+
RAISE EXCEPTION 'Root entity cannot be empty';
|
|
63
|
+
END IF;
|
|
64
|
+
|
|
65
|
+
-- Insert or update subscribable
|
|
66
|
+
INSERT INTO dzql.subscribables (
|
|
67
|
+
name,
|
|
68
|
+
permission_paths,
|
|
69
|
+
param_schema,
|
|
70
|
+
root_entity,
|
|
71
|
+
relations,
|
|
72
|
+
created_at,
|
|
73
|
+
updated_at
|
|
74
|
+
) VALUES (
|
|
75
|
+
p_name,
|
|
76
|
+
COALESCE(p_permission_paths, '{}'::jsonb),
|
|
77
|
+
COALESCE(p_param_schema, '{}'::jsonb),
|
|
78
|
+
p_root_entity,
|
|
79
|
+
COALESCE(p_relations, '{}'::jsonb),
|
|
80
|
+
NOW(),
|
|
81
|
+
NOW()
|
|
82
|
+
)
|
|
83
|
+
ON CONFLICT (name) DO UPDATE SET
|
|
84
|
+
permission_paths = EXCLUDED.permission_paths,
|
|
85
|
+
param_schema = EXCLUDED.param_schema,
|
|
86
|
+
root_entity = EXCLUDED.root_entity,
|
|
87
|
+
relations = EXCLUDED.relations,
|
|
88
|
+
updated_at = NOW();
|
|
89
|
+
|
|
90
|
+
v_result := format('Subscribable "%s" registered successfully', p_name);
|
|
91
|
+
|
|
92
|
+
RAISE NOTICE '%', v_result;
|
|
93
|
+
|
|
94
|
+
RETURN v_result;
|
|
95
|
+
END;
|
|
96
|
+
$$ LANGUAGE plpgsql;
|
|
97
|
+
|
|
98
|
+
COMMENT ON FUNCTION dzql.register_subscribable IS
|
|
99
|
+
'Register a subscribable document definition. Used by the compiler to store subscribable metadata.';
|
|
100
|
+
|
|
101
|
+
-- ============================================================================
|
|
102
|
+
-- Helper Functions
|
|
103
|
+
-- ============================================================================
|
|
104
|
+
|
|
105
|
+
-- Get all registered subscribables
|
|
106
|
+
CREATE OR REPLACE FUNCTION dzql.get_subscribables()
|
|
107
|
+
RETURNS TABLE (
|
|
108
|
+
name TEXT,
|
|
109
|
+
permission_paths JSONB,
|
|
110
|
+
param_schema JSONB,
|
|
111
|
+
root_entity TEXT,
|
|
112
|
+
relations JSONB,
|
|
113
|
+
created_at TIMESTAMPTZ,
|
|
114
|
+
updated_at TIMESTAMPTZ
|
|
115
|
+
) AS $$
|
|
116
|
+
BEGIN
|
|
117
|
+
RETURN QUERY
|
|
118
|
+
SELECT
|
|
119
|
+
s.name,
|
|
120
|
+
s.permission_paths,
|
|
121
|
+
s.param_schema,
|
|
122
|
+
s.root_entity,
|
|
123
|
+
s.relations,
|
|
124
|
+
s.created_at,
|
|
125
|
+
s.updated_at
|
|
126
|
+
FROM dzql.subscribables s
|
|
127
|
+
ORDER BY s.name;
|
|
128
|
+
END;
|
|
129
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
130
|
+
|
|
131
|
+
COMMENT ON FUNCTION dzql.get_subscribables IS
|
|
132
|
+
'Retrieve all registered subscribables';
|
|
133
|
+
|
|
134
|
+
-- Get subscribable by name
|
|
135
|
+
CREATE OR REPLACE FUNCTION dzql.get_subscribable(p_name TEXT)
|
|
136
|
+
RETURNS TABLE (
|
|
137
|
+
name TEXT,
|
|
138
|
+
permission_paths JSONB,
|
|
139
|
+
param_schema JSONB,
|
|
140
|
+
root_entity TEXT,
|
|
141
|
+
relations JSONB,
|
|
142
|
+
created_at TIMESTAMPTZ,
|
|
143
|
+
updated_at TIMESTAMPTZ
|
|
144
|
+
) AS $$
|
|
145
|
+
BEGIN
|
|
146
|
+
RETURN QUERY
|
|
147
|
+
SELECT
|
|
148
|
+
s.name,
|
|
149
|
+
s.permission_paths,
|
|
150
|
+
s.param_schema,
|
|
151
|
+
s.root_entity,
|
|
152
|
+
s.relations,
|
|
153
|
+
s.created_at,
|
|
154
|
+
s.updated_at
|
|
155
|
+
FROM dzql.subscribables s
|
|
156
|
+
WHERE s.name = p_name;
|
|
157
|
+
END;
|
|
158
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
159
|
+
|
|
160
|
+
COMMENT ON FUNCTION dzql.get_subscribable IS
|
|
161
|
+
'Retrieve a specific subscribable by name';
|
|
162
|
+
|
|
163
|
+
-- Get subscribables by root entity
|
|
164
|
+
CREATE OR REPLACE FUNCTION dzql.get_subscribables_by_entity(p_entity TEXT)
|
|
165
|
+
RETURNS TABLE (
|
|
166
|
+
name TEXT,
|
|
167
|
+
permission_paths JSONB,
|
|
168
|
+
param_schema JSONB,
|
|
169
|
+
root_entity TEXT,
|
|
170
|
+
relations JSONB,
|
|
171
|
+
created_at TIMESTAMPTZ,
|
|
172
|
+
updated_at TIMESTAMPTZ
|
|
173
|
+
) AS $$
|
|
174
|
+
BEGIN
|
|
175
|
+
RETURN QUERY
|
|
176
|
+
SELECT
|
|
177
|
+
s.name,
|
|
178
|
+
s.permission_paths,
|
|
179
|
+
s.param_schema,
|
|
180
|
+
s.root_entity,
|
|
181
|
+
s.relations,
|
|
182
|
+
s.created_at,
|
|
183
|
+
s.updated_at
|
|
184
|
+
FROM dzql.subscribables s
|
|
185
|
+
WHERE s.root_entity = p_entity
|
|
186
|
+
ORDER BY s.name;
|
|
187
|
+
END;
|
|
188
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
189
|
+
|
|
190
|
+
COMMENT ON FUNCTION dzql.get_subscribables_by_entity IS
|
|
191
|
+
'Retrieve all subscribables for a given root entity';
|
|
192
|
+
|
|
193
|
+
-- Delete subscribable
|
|
194
|
+
CREATE OR REPLACE FUNCTION dzql.delete_subscribable(p_name TEXT)
|
|
195
|
+
RETURNS TEXT AS $$
|
|
196
|
+
DECLARE
|
|
197
|
+
v_result TEXT;
|
|
198
|
+
BEGIN
|
|
199
|
+
DELETE FROM dzql.subscribables
|
|
200
|
+
WHERE name = p_name;
|
|
201
|
+
|
|
202
|
+
IF FOUND THEN
|
|
203
|
+
v_result := format('Subscribable "%s" deleted successfully', p_name);
|
|
204
|
+
ELSE
|
|
205
|
+
v_result := format('Subscribable "%s" not found', p_name);
|
|
206
|
+
END IF;
|
|
207
|
+
|
|
208
|
+
RAISE NOTICE '%', v_result;
|
|
209
|
+
|
|
210
|
+
RETURN v_result;
|
|
211
|
+
END;
|
|
212
|
+
$$ LANGUAGE plpgsql;
|
|
213
|
+
|
|
214
|
+
COMMENT ON FUNCTION dzql.delete_subscribable IS
|
|
215
|
+
'Delete a subscribable by name';
|
|
216
|
+
|
|
217
|
+
-- ============================================================================
|
|
218
|
+
-- Verification
|
|
219
|
+
-- ============================================================================
|
|
220
|
+
|
|
221
|
+
DO $$
|
|
222
|
+
BEGIN
|
|
223
|
+
RAISE NOTICE 'Migration 009: Live Query Subscriptions - Complete';
|
|
224
|
+
RAISE NOTICE 'Subscribables table created';
|
|
225
|
+
RAISE NOTICE 'Helper functions installed';
|
|
226
|
+
RAISE NOTICE '';
|
|
227
|
+
RAISE NOTICE 'Usage:';
|
|
228
|
+
RAISE NOTICE ' SELECT dzql.register_subscribable(''name'', permissions, params, root, relations);';
|
|
229
|
+
RAISE NOTICE ' SELECT * FROM dzql.get_subscribables();';
|
|
230
|
+
END $$;
|