dzql 0.6.3 → 0.6.6
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 +33 -0
- package/docs/for_ai.md +14 -18
- package/docs/project-setup.md +15 -14
- package/package.json +28 -6
- package/src/cli/codegen/client.ts +5 -6
- package/src/cli/codegen/subscribable_store.ts +5 -5
- package/src/runtime/ws.ts +16 -15
- package/.env.sample +0 -28
- package/compose.yml +0 -28
- package/dist/client/index.ts +0 -1
- package/dist/client/stores/useMyProfileStore.ts +0 -114
- package/dist/client/stores/useOrgDashboardStore.ts +0 -131
- package/dist/client/stores/useVenueDetailStore.ts +0 -117
- package/dist/client/ws.ts +0 -716
- package/dist/db/migrations/000_core.sql +0 -92
- package/dist/db/migrations/20260101T235039268Z_schema.sql +0 -3020
- package/dist/db/migrations/20260101T235039268Z_subscribables.sql +0 -371
- package/dist/runtime/manifest.json +0 -1562
- package/examples/blog.ts +0 -50
- package/examples/invalid.ts +0 -18
- package/examples/venues.js +0 -485
- package/tests/client.test.ts +0 -38
- package/tests/codegen.test.ts +0 -71
- package/tests/compiler.test.ts +0 -45
- package/tests/graph_rules.test.ts +0 -173
- package/tests/integration/db.test.ts +0 -174
- package/tests/integration/e2e.test.ts +0 -65
- package/tests/integration/features.test.ts +0 -922
- package/tests/integration/full_stack.test.ts +0 -262
- package/tests/integration/setup.ts +0 -45
- package/tests/ir.test.ts +0 -32
- package/tests/namespace.test.ts +0 -395
- package/tests/permissions.test.ts +0 -55
- package/tests/pinia.test.ts +0 -48
- package/tests/realtime.test.ts +0 -22
- package/tests/runtime.test.ts +0 -80
- package/tests/subscribable_gen.test.ts +0 -72
- package/tests/subscribable_reactivity.test.ts +0 -258
- package/tests/venues_gen.test.ts +0 -25
- package/tsconfig.json +0 -20
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,3020 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
3
|
-
id serial PRIMARY KEY,
|
|
4
|
-
name text NOT NULL,
|
|
5
|
-
email text UNIQUE NOT NULL,
|
|
6
|
-
password_hash text NOT NULL,
|
|
7
|
-
created_at timestamptz DEFAULT now()
|
|
8
|
-
);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_users(p_user_id int, p_data jsonb)
|
|
12
|
-
RETURNS jsonb
|
|
13
|
-
LANGUAGE plpgsql
|
|
14
|
-
SECURITY DEFINER
|
|
15
|
-
SET search_path = dzql_v2, public
|
|
16
|
-
AS $$
|
|
17
|
-
DECLARE
|
|
18
|
-
v_result jsonb;
|
|
19
|
-
v_old_data jsonb;
|
|
20
|
-
v_commit_id bigint;
|
|
21
|
-
v_op text;
|
|
22
|
-
|
|
23
|
-
BEGIN
|
|
24
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
28
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM users WHERE id = (p_data->>'id')::int) THEN
|
|
29
|
-
v_op := 'update';
|
|
30
|
-
|
|
31
|
-
-- Fetch old data for update rules/events
|
|
32
|
-
SELECT to_jsonb(users.*) INTO v_old_data FROM users WHERE id = (p_data->>'id')::int;
|
|
33
|
-
|
|
34
|
-
IF NOT ((p_data->>'id')::int = p_user_id) THEN
|
|
35
|
-
RAISE EXCEPTION 'permission_denied';
|
|
36
|
-
END IF;
|
|
37
|
-
|
|
38
|
-
-- Perform Partial Update
|
|
39
|
-
UPDATE users SET
|
|
40
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
41
|
-
email = CASE WHEN (p_data ? 'email') THEN (p_data->>'email') ELSE email END,
|
|
42
|
-
password_hash = CASE WHEN (p_data ? 'password_hash') THEN (p_data->>'password_hash') ELSE password_hash END,
|
|
43
|
-
created_at = CASE WHEN (p_data ? 'created_at') THEN (p_data->>'created_at')::timestamptz ELSE created_at END
|
|
44
|
-
WHERE id = (p_data->>'id')::int
|
|
45
|
-
RETURNING to_jsonb(users.*) INTO v_result;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
ELSE
|
|
50
|
-
v_op := 'insert';
|
|
51
|
-
IF NOT (TRUE) THEN
|
|
52
|
-
RAISE EXCEPTION 'permission_denied';
|
|
53
|
-
END IF;
|
|
54
|
-
|
|
55
|
-
-- Perform Insert
|
|
56
|
-
INSERT INTO users (name, email, password_hash, created_at)
|
|
57
|
-
VALUES ((p_data->>'name'), (p_data->>'email'), (p_data->>'password_hash'), (p_data->>'created_at')::timestamptz)
|
|
58
|
-
RETURNING to_jsonb(users.*) INTO v_result;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
END IF;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-- Emit Event
|
|
66
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
67
|
-
VALUES (
|
|
68
|
-
v_commit_id,
|
|
69
|
-
'users',
|
|
70
|
-
v_op,
|
|
71
|
-
jsonb_build_object('id', v_result->'id'),
|
|
72
|
-
v_result,
|
|
73
|
-
v_old_data, -- NULL for insert
|
|
74
|
-
p_user_id
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
-- Notify Runtime
|
|
78
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
79
|
-
|
|
80
|
-
-- Remove hidden fields before returning to client
|
|
81
|
-
RETURN v_result - ARRAY['password_hash'];
|
|
82
|
-
END;
|
|
83
|
-
$$;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_users(p_user_id int, p_pk jsonb)
|
|
87
|
-
RETURNS jsonb
|
|
88
|
-
LANGUAGE plpgsql
|
|
89
|
-
SECURITY DEFINER
|
|
90
|
-
SET search_path = dzql_v2, public
|
|
91
|
-
AS $$
|
|
92
|
-
DECLARE
|
|
93
|
-
v_old_data jsonb;
|
|
94
|
-
v_commit_id bigint;
|
|
95
|
-
BEGIN
|
|
96
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
97
|
-
|
|
98
|
-
-- Fetch old data FIRST for permission check
|
|
99
|
-
SELECT to_jsonb(users.*) INTO v_old_data FROM users WHERE id = (p_pk->>'id')::int;
|
|
100
|
-
|
|
101
|
-
IF v_old_data IS NULL THEN
|
|
102
|
-
RAISE EXCEPTION 'not_found';
|
|
103
|
-
END IF;
|
|
104
|
-
|
|
105
|
-
-- Permission Check (Delete)
|
|
106
|
-
IF NOT ((v_old_data->>'id')::int = p_user_id) THEN
|
|
107
|
-
RAISE EXCEPTION 'permission_denied';
|
|
108
|
-
END IF;
|
|
109
|
-
|
|
110
|
-
-- Graph Rules (Pre-delete cascades)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
-- Perform Delete
|
|
114
|
-
DELETE FROM users WHERE id = (p_pk->>'id')::int;
|
|
115
|
-
|
|
116
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
117
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
118
|
-
VALUES (
|
|
119
|
-
v_commit_id,
|
|
120
|
-
'users',
|
|
121
|
-
'delete',
|
|
122
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
123
|
-
v_old_data, -- Include full data for subscription resolution
|
|
124
|
-
v_old_data,
|
|
125
|
-
p_user_id
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
-- Notify Runtime
|
|
129
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
130
|
-
|
|
131
|
-
-- Remove hidden fields before returning to client
|
|
132
|
-
RETURN v_old_data - ARRAY['password_hash'];
|
|
133
|
-
END;
|
|
134
|
-
$$;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_users(p_user_id int, p_pk jsonb)
|
|
138
|
-
RETURNS jsonb
|
|
139
|
-
LANGUAGE plpgsql
|
|
140
|
-
SECURITY DEFINER
|
|
141
|
-
SET search_path = dzql_v2, public
|
|
142
|
-
AS $$
|
|
143
|
-
DECLARE
|
|
144
|
-
v_result jsonb;
|
|
145
|
-
BEGIN
|
|
146
|
-
SELECT jsonb_build_object('id', users.id, 'name', users.name, 'email', users.email, 'created_at', users.created_at) INTO v_result
|
|
147
|
-
FROM users
|
|
148
|
-
WHERE id = (p_pk->>'id')::int
|
|
149
|
-
AND (TRUE);
|
|
150
|
-
|
|
151
|
-
IF v_result IS NULL THEN
|
|
152
|
-
RETURN NULL;
|
|
153
|
-
END IF;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
RETURN v_result;
|
|
157
|
-
END;
|
|
158
|
-
$$;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_users(p_user_id int, p_query jsonb)
|
|
162
|
-
RETURNS jsonb
|
|
163
|
-
LANGUAGE plpgsql
|
|
164
|
-
SECURITY DEFINER
|
|
165
|
-
SET search_path = dzql_v2, public
|
|
166
|
-
AS $$
|
|
167
|
-
DECLARE
|
|
168
|
-
v_results jsonb;
|
|
169
|
-
v_filters jsonb;
|
|
170
|
-
v_sort_field text;
|
|
171
|
-
v_sort_order text;
|
|
172
|
-
v_where_clause text := '';
|
|
173
|
-
v_field text;
|
|
174
|
-
v_filter jsonb;
|
|
175
|
-
v_operator text;
|
|
176
|
-
v_value jsonb;
|
|
177
|
-
BEGIN
|
|
178
|
-
-- Extract query parameters
|
|
179
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
180
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
181
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
182
|
-
|
|
183
|
-
-- Build WHERE clause from filters
|
|
184
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
185
|
-
LOOP
|
|
186
|
-
-- Handle simple value (exact match)
|
|
187
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
188
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
189
|
-
ELSE
|
|
190
|
-
-- Handle operator-based filters
|
|
191
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
192
|
-
LOOP
|
|
193
|
-
CASE v_operator
|
|
194
|
-
WHEN 'eq' THEN
|
|
195
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
196
|
-
WHEN 'ne' THEN
|
|
197
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
198
|
-
WHEN 'gt' THEN
|
|
199
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
200
|
-
WHEN 'gte' THEN
|
|
201
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
202
|
-
WHEN 'lt' THEN
|
|
203
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
204
|
-
WHEN 'lte' THEN
|
|
205
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
206
|
-
WHEN 'in' THEN
|
|
207
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
208
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
209
|
-
WHEN 'not_in' THEN
|
|
210
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
211
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
212
|
-
WHEN 'like' THEN
|
|
213
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
214
|
-
WHEN 'ilike' THEN
|
|
215
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
216
|
-
WHEN 'is_null' THEN
|
|
217
|
-
IF (v_value::text = 'true') THEN
|
|
218
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
219
|
-
END IF;
|
|
220
|
-
WHEN 'not_null' THEN
|
|
221
|
-
IF (v_value::text = 'true') THEN
|
|
222
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
223
|
-
END IF;
|
|
224
|
-
ELSE
|
|
225
|
-
-- Unknown operator, skip
|
|
226
|
-
END CASE;
|
|
227
|
-
END LOOP;
|
|
228
|
-
END IF;
|
|
229
|
-
END LOOP;
|
|
230
|
-
|
|
231
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
232
|
-
EXECUTE format('
|
|
233
|
-
SELECT COALESCE(jsonb_agg(jsonb_build_object(''id'', t.id, ''name'', t.name, ''email'', t.email, ''created_at'', t.created_at)), ''[]''::jsonb)
|
|
234
|
-
FROM (
|
|
235
|
-
SELECT * FROM users
|
|
236
|
-
WHERE (TRUE) %s
|
|
237
|
-
ORDER BY %I %s
|
|
238
|
-
LIMIT %L OFFSET %L
|
|
239
|
-
) t
|
|
240
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
241
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
242
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
243
|
-
INTO v_results;
|
|
244
|
-
|
|
245
|
-
RETURN v_results;
|
|
246
|
-
END;
|
|
247
|
-
$$;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
CREATE TABLE IF NOT EXISTS organisations (
|
|
251
|
-
id serial PRIMARY KEY,
|
|
252
|
-
name text UNIQUE NOT NULL,
|
|
253
|
-
description text
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_organisations(p_user_id int, p_data jsonb)
|
|
258
|
-
RETURNS jsonb
|
|
259
|
-
LANGUAGE plpgsql
|
|
260
|
-
SECURITY DEFINER
|
|
261
|
-
SET search_path = dzql_v2, public
|
|
262
|
-
AS $$
|
|
263
|
-
DECLARE
|
|
264
|
-
v_result jsonb;
|
|
265
|
-
v_old_data jsonb;
|
|
266
|
-
v_commit_id bigint;
|
|
267
|
-
v_op text;
|
|
268
|
-
|
|
269
|
-
BEGIN
|
|
270
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
274
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM organisations WHERE id = (p_data->>'id')::int) THEN
|
|
275
|
-
v_op := 'update';
|
|
276
|
-
|
|
277
|
-
-- Fetch old data for update rules/events
|
|
278
|
-
SELECT to_jsonb(organisations.*) INTO v_old_data FROM organisations WHERE id = (p_data->>'id')::int;
|
|
279
|
-
|
|
280
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
281
|
-
RAISE EXCEPTION 'permission_denied';
|
|
282
|
-
END IF;
|
|
283
|
-
|
|
284
|
-
-- Perform Partial Update
|
|
285
|
-
UPDATE organisations SET
|
|
286
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
287
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END
|
|
288
|
-
WHERE id = (p_data->>'id')::int
|
|
289
|
-
RETURNING to_jsonb(organisations.*) INTO v_result;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
ELSE
|
|
294
|
-
v_op := 'insert';
|
|
295
|
-
IF NOT (TRUE) THEN
|
|
296
|
-
RAISE EXCEPTION 'permission_denied';
|
|
297
|
-
END IF;
|
|
298
|
-
|
|
299
|
-
-- Perform Insert
|
|
300
|
-
INSERT INTO organisations (name, description)
|
|
301
|
-
VALUES ((p_data->>'name'), (p_data->>'description'))
|
|
302
|
-
RETURNING to_jsonb(organisations.*) INTO v_result;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
-- Graph Rule: Create acts_for
|
|
306
|
-
INSERT INTO acts_for (user_id, org_id, valid_from)
|
|
307
|
-
VALUES (p_user_id, (v_result->>'id')::int, CURRENT_DATE);
|
|
308
|
-
|
|
309
|
-
END IF;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
-- Emit Event
|
|
314
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
315
|
-
VALUES (
|
|
316
|
-
v_commit_id,
|
|
317
|
-
'organisations',
|
|
318
|
-
v_op,
|
|
319
|
-
jsonb_build_object('id', v_result->'id'),
|
|
320
|
-
v_result,
|
|
321
|
-
v_old_data, -- NULL for insert
|
|
322
|
-
p_user_id
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
-- Notify Runtime
|
|
326
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
327
|
-
|
|
328
|
-
-- Remove hidden fields before returning to client
|
|
329
|
-
RETURN v_result;
|
|
330
|
-
END;
|
|
331
|
-
$$;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_organisations(p_user_id int, p_pk jsonb)
|
|
335
|
-
RETURNS jsonb
|
|
336
|
-
LANGUAGE plpgsql
|
|
337
|
-
SECURITY DEFINER
|
|
338
|
-
SET search_path = dzql_v2, public
|
|
339
|
-
AS $$
|
|
340
|
-
DECLARE
|
|
341
|
-
v_old_data jsonb;
|
|
342
|
-
v_commit_id bigint;
|
|
343
|
-
BEGIN
|
|
344
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
345
|
-
|
|
346
|
-
-- Fetch old data FIRST for permission check
|
|
347
|
-
SELECT to_jsonb(organisations.*) INTO v_old_data FROM organisations WHERE id = (p_pk->>'id')::int;
|
|
348
|
-
|
|
349
|
-
IF v_old_data IS NULL THEN
|
|
350
|
-
RAISE EXCEPTION 'not_found';
|
|
351
|
-
END IF;
|
|
352
|
-
|
|
353
|
-
-- Permission Check (Delete)
|
|
354
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
355
|
-
RAISE EXCEPTION 'permission_denied';
|
|
356
|
-
END IF;
|
|
357
|
-
|
|
358
|
-
-- Graph Rules (Pre-delete cascades)
|
|
359
|
-
|
|
360
|
-
-- Graph Rule: Delete acts_for
|
|
361
|
-
DELETE FROM acts_for WHERE org_id = (v_old_data->>'id')::int;
|
|
362
|
-
|
|
363
|
-
-- Graph Rule: Delete venues
|
|
364
|
-
DELETE FROM venues WHERE org_id = (v_old_data->>'id')::int;
|
|
365
|
-
|
|
366
|
-
-- Graph Rule: Update packages
|
|
367
|
-
UPDATE packages
|
|
368
|
-
SET sponsor_org_id = (v_old_data->>'id')::int
|
|
369
|
-
WHERE TRUE;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
-- Perform Delete
|
|
373
|
-
DELETE FROM organisations WHERE id = (p_pk->>'id')::int;
|
|
374
|
-
|
|
375
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
376
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
377
|
-
VALUES (
|
|
378
|
-
v_commit_id,
|
|
379
|
-
'organisations',
|
|
380
|
-
'delete',
|
|
381
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
382
|
-
v_old_data, -- Include full data for subscription resolution
|
|
383
|
-
v_old_data,
|
|
384
|
-
p_user_id
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
-- Notify Runtime
|
|
388
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
389
|
-
|
|
390
|
-
-- Remove hidden fields before returning to client
|
|
391
|
-
RETURN v_old_data;
|
|
392
|
-
END;
|
|
393
|
-
$$;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_organisations(p_user_id int, p_pk jsonb)
|
|
397
|
-
RETURNS jsonb
|
|
398
|
-
LANGUAGE plpgsql
|
|
399
|
-
SECURITY DEFINER
|
|
400
|
-
SET search_path = dzql_v2, public
|
|
401
|
-
AS $$
|
|
402
|
-
DECLARE
|
|
403
|
-
v_result jsonb;
|
|
404
|
-
BEGIN
|
|
405
|
-
SELECT to_jsonb(organisations.*) INTO v_result
|
|
406
|
-
FROM organisations
|
|
407
|
-
WHERE id = (p_pk->>'id')::int
|
|
408
|
-
AND (TRUE);
|
|
409
|
-
|
|
410
|
-
IF v_result IS NULL THEN
|
|
411
|
-
RETURN NULL;
|
|
412
|
-
END IF;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
RETURN v_result;
|
|
416
|
-
END;
|
|
417
|
-
$$;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_organisations(p_user_id int, p_query jsonb)
|
|
421
|
-
RETURNS jsonb
|
|
422
|
-
LANGUAGE plpgsql
|
|
423
|
-
SECURITY DEFINER
|
|
424
|
-
SET search_path = dzql_v2, public
|
|
425
|
-
AS $$
|
|
426
|
-
DECLARE
|
|
427
|
-
v_results jsonb;
|
|
428
|
-
v_filters jsonb;
|
|
429
|
-
v_sort_field text;
|
|
430
|
-
v_sort_order text;
|
|
431
|
-
v_where_clause text := '';
|
|
432
|
-
v_field text;
|
|
433
|
-
v_filter jsonb;
|
|
434
|
-
v_operator text;
|
|
435
|
-
v_value jsonb;
|
|
436
|
-
BEGIN
|
|
437
|
-
-- Extract query parameters
|
|
438
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
439
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
440
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
441
|
-
|
|
442
|
-
-- Build WHERE clause from filters
|
|
443
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
444
|
-
LOOP
|
|
445
|
-
-- Handle simple value (exact match)
|
|
446
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
447
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
448
|
-
ELSE
|
|
449
|
-
-- Handle operator-based filters
|
|
450
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
451
|
-
LOOP
|
|
452
|
-
CASE v_operator
|
|
453
|
-
WHEN 'eq' THEN
|
|
454
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
455
|
-
WHEN 'ne' THEN
|
|
456
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
457
|
-
WHEN 'gt' THEN
|
|
458
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
459
|
-
WHEN 'gte' THEN
|
|
460
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
461
|
-
WHEN 'lt' THEN
|
|
462
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
463
|
-
WHEN 'lte' THEN
|
|
464
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
465
|
-
WHEN 'in' THEN
|
|
466
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
467
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
468
|
-
WHEN 'not_in' THEN
|
|
469
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
470
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
471
|
-
WHEN 'like' THEN
|
|
472
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
473
|
-
WHEN 'ilike' THEN
|
|
474
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
475
|
-
WHEN 'is_null' THEN
|
|
476
|
-
IF (v_value::text = 'true') THEN
|
|
477
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
478
|
-
END IF;
|
|
479
|
-
WHEN 'not_null' THEN
|
|
480
|
-
IF (v_value::text = 'true') THEN
|
|
481
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
482
|
-
END IF;
|
|
483
|
-
ELSE
|
|
484
|
-
-- Unknown operator, skip
|
|
485
|
-
END CASE;
|
|
486
|
-
END LOOP;
|
|
487
|
-
END IF;
|
|
488
|
-
END LOOP;
|
|
489
|
-
|
|
490
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
491
|
-
EXECUTE format('
|
|
492
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
493
|
-
FROM (
|
|
494
|
-
SELECT * FROM organisations
|
|
495
|
-
WHERE (TRUE) %s
|
|
496
|
-
ORDER BY %I %s
|
|
497
|
-
LIMIT %L OFFSET %L
|
|
498
|
-
) t
|
|
499
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
500
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
501
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
502
|
-
INTO v_results;
|
|
503
|
-
|
|
504
|
-
RETURN v_results;
|
|
505
|
-
END;
|
|
506
|
-
$$;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
CREATE TABLE IF NOT EXISTS acts_for (
|
|
510
|
-
user_id int NOT NULL REFERENCES users(id),
|
|
511
|
-
org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
512
|
-
valid_from date NOT NULL DEFAULT current_date,
|
|
513
|
-
valid_to date,
|
|
514
|
-
active boolean DEFAULT true
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_acts_for(p_user_id int, p_data jsonb)
|
|
519
|
-
RETURNS jsonb
|
|
520
|
-
LANGUAGE plpgsql
|
|
521
|
-
SECURITY DEFINER
|
|
522
|
-
SET search_path = dzql_v2, public
|
|
523
|
-
AS $$
|
|
524
|
-
DECLARE
|
|
525
|
-
v_result jsonb;
|
|
526
|
-
v_old_data jsonb;
|
|
527
|
-
v_commit_id bigint;
|
|
528
|
-
v_op text;
|
|
529
|
-
|
|
530
|
-
BEGIN
|
|
531
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
535
|
-
IF ((p_data->>'user_id') IS NOT NULL AND (p_data->>'org_id') IS NOT NULL AND (p_data->>'valid_from') IS NOT NULL) AND EXISTS(SELECT 1 FROM acts_for WHERE user_id = (p_data->>'user_id')::int AND org_id = (p_data->>'org_id')::int AND valid_from = (p_data->>'valid_from')::date) THEN
|
|
536
|
-
v_op := 'update';
|
|
537
|
-
|
|
538
|
-
-- Fetch old data for update rules/events
|
|
539
|
-
SELECT to_jsonb(acts_for.*) INTO v_old_data FROM acts_for WHERE user_id = (p_data->>'user_id')::int AND org_id = (p_data->>'org_id')::int AND valid_from = (p_data->>'valid_from')::date;
|
|
540
|
-
|
|
541
|
-
IF NOT (TRUE) THEN
|
|
542
|
-
RAISE EXCEPTION 'permission_denied';
|
|
543
|
-
END IF;
|
|
544
|
-
|
|
545
|
-
-- Perform Partial Update
|
|
546
|
-
UPDATE acts_for SET
|
|
547
|
-
valid_to = CASE WHEN (p_data ? 'valid_to') THEN (p_data->>'valid_to')::date ELSE valid_to END,
|
|
548
|
-
active = CASE WHEN (p_data ? 'active') THEN (p_data->>'active')::boolean ELSE active END
|
|
549
|
-
WHERE user_id = (p_data->>'user_id')::int AND org_id = (p_data->>'org_id')::int AND valid_from = (p_data->>'valid_from')::date
|
|
550
|
-
RETURNING to_jsonb(acts_for.*) INTO v_result;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
ELSE
|
|
555
|
-
v_op := 'insert';
|
|
556
|
-
IF NOT (TRUE) THEN
|
|
557
|
-
RAISE EXCEPTION 'permission_denied';
|
|
558
|
-
END IF;
|
|
559
|
-
|
|
560
|
-
-- Perform Insert
|
|
561
|
-
INSERT INTO acts_for (user_id, org_id, valid_from, valid_to, active)
|
|
562
|
-
VALUES ((p_data->>'user_id')::int, (p_data->>'org_id')::int, (p_data->>'valid_from')::date, (p_data->>'valid_to')::date, (p_data->>'active')::boolean)
|
|
563
|
-
RETURNING to_jsonb(acts_for.*) INTO v_result;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
END IF;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
-- Emit Event
|
|
571
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
572
|
-
VALUES (
|
|
573
|
-
v_commit_id,
|
|
574
|
-
'acts_for',
|
|
575
|
-
v_op,
|
|
576
|
-
jsonb_build_object('user_id', v_result->'user_id', 'org_id', v_result->'org_id', 'valid_from', v_result->'valid_from'),
|
|
577
|
-
v_result,
|
|
578
|
-
v_old_data, -- NULL for insert
|
|
579
|
-
p_user_id
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
-- Notify Runtime
|
|
583
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
584
|
-
|
|
585
|
-
-- Remove hidden fields before returning to client
|
|
586
|
-
RETURN v_result;
|
|
587
|
-
END;
|
|
588
|
-
$$;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_acts_for(p_user_id int, p_pk jsonb)
|
|
592
|
-
RETURNS jsonb
|
|
593
|
-
LANGUAGE plpgsql
|
|
594
|
-
SECURITY DEFINER
|
|
595
|
-
SET search_path = dzql_v2, public
|
|
596
|
-
AS $$
|
|
597
|
-
DECLARE
|
|
598
|
-
v_old_data jsonb;
|
|
599
|
-
v_commit_id bigint;
|
|
600
|
-
BEGIN
|
|
601
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
602
|
-
|
|
603
|
-
-- Fetch old data FIRST for permission check
|
|
604
|
-
SELECT to_jsonb(acts_for.*) INTO v_old_data FROM acts_for WHERE user_id = (p_pk->>'user_id')::int AND org_id = (p_pk->>'org_id')::int AND valid_from = (p_pk->>'valid_from')::date;
|
|
605
|
-
|
|
606
|
-
IF v_old_data IS NULL THEN
|
|
607
|
-
RAISE EXCEPTION 'not_found';
|
|
608
|
-
END IF;
|
|
609
|
-
|
|
610
|
-
-- Permission Check (Delete)
|
|
611
|
-
IF NOT (TRUE) THEN
|
|
612
|
-
RAISE EXCEPTION 'permission_denied';
|
|
613
|
-
END IF;
|
|
614
|
-
|
|
615
|
-
-- Graph Rules (Pre-delete cascades)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
-- Perform Delete
|
|
619
|
-
DELETE FROM acts_for WHERE user_id = (p_pk->>'user_id')::int AND org_id = (p_pk->>'org_id')::int AND valid_from = (p_pk->>'valid_from')::date;
|
|
620
|
-
|
|
621
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
622
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
623
|
-
VALUES (
|
|
624
|
-
v_commit_id,
|
|
625
|
-
'acts_for',
|
|
626
|
-
'delete',
|
|
627
|
-
jsonb_build_object('user_id', v_old_data->'user_id', 'org_id', v_old_data->'org_id', 'valid_from', v_old_data->'valid_from'),
|
|
628
|
-
v_old_data, -- Include full data for subscription resolution
|
|
629
|
-
v_old_data,
|
|
630
|
-
p_user_id
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
-- Notify Runtime
|
|
634
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
635
|
-
|
|
636
|
-
-- Remove hidden fields before returning to client
|
|
637
|
-
RETURN v_old_data;
|
|
638
|
-
END;
|
|
639
|
-
$$;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_acts_for(p_user_id int, p_pk jsonb)
|
|
643
|
-
RETURNS jsonb
|
|
644
|
-
LANGUAGE plpgsql
|
|
645
|
-
SECURITY DEFINER
|
|
646
|
-
SET search_path = dzql_v2, public
|
|
647
|
-
AS $$
|
|
648
|
-
DECLARE
|
|
649
|
-
v_result jsonb;
|
|
650
|
-
BEGIN
|
|
651
|
-
SELECT to_jsonb(acts_for.*) INTO v_result
|
|
652
|
-
FROM acts_for
|
|
653
|
-
WHERE user_id = (p_pk->>'user_id')::int AND org_id = (p_pk->>'org_id')::int AND valid_from = (p_pk->>'valid_from')::date
|
|
654
|
-
AND (TRUE);
|
|
655
|
-
|
|
656
|
-
IF v_result IS NULL THEN
|
|
657
|
-
RETURN NULL;
|
|
658
|
-
END IF;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
RETURN v_result;
|
|
662
|
-
END;
|
|
663
|
-
$$;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_acts_for(p_user_id int, p_query jsonb)
|
|
667
|
-
RETURNS jsonb
|
|
668
|
-
LANGUAGE plpgsql
|
|
669
|
-
SECURITY DEFINER
|
|
670
|
-
SET search_path = dzql_v2, public
|
|
671
|
-
AS $$
|
|
672
|
-
DECLARE
|
|
673
|
-
v_results jsonb;
|
|
674
|
-
v_filters jsonb;
|
|
675
|
-
v_sort_field text;
|
|
676
|
-
v_sort_order text;
|
|
677
|
-
v_where_clause text := '';
|
|
678
|
-
v_field text;
|
|
679
|
-
v_filter jsonb;
|
|
680
|
-
v_operator text;
|
|
681
|
-
v_value jsonb;
|
|
682
|
-
BEGIN
|
|
683
|
-
-- Extract query parameters
|
|
684
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
685
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'org_id');
|
|
686
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
687
|
-
|
|
688
|
-
-- Build WHERE clause from filters
|
|
689
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
690
|
-
LOOP
|
|
691
|
-
-- Handle simple value (exact match)
|
|
692
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
693
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
694
|
-
ELSE
|
|
695
|
-
-- Handle operator-based filters
|
|
696
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
697
|
-
LOOP
|
|
698
|
-
CASE v_operator
|
|
699
|
-
WHEN 'eq' THEN
|
|
700
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
701
|
-
WHEN 'ne' THEN
|
|
702
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
703
|
-
WHEN 'gt' THEN
|
|
704
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
705
|
-
WHEN 'gte' THEN
|
|
706
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
707
|
-
WHEN 'lt' THEN
|
|
708
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
709
|
-
WHEN 'lte' THEN
|
|
710
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
711
|
-
WHEN 'in' THEN
|
|
712
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
713
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
714
|
-
WHEN 'not_in' THEN
|
|
715
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
716
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
717
|
-
WHEN 'like' THEN
|
|
718
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
719
|
-
WHEN 'ilike' THEN
|
|
720
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
721
|
-
WHEN 'is_null' THEN
|
|
722
|
-
IF (v_value::text = 'true') THEN
|
|
723
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
724
|
-
END IF;
|
|
725
|
-
WHEN 'not_null' THEN
|
|
726
|
-
IF (v_value::text = 'true') THEN
|
|
727
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
728
|
-
END IF;
|
|
729
|
-
ELSE
|
|
730
|
-
-- Unknown operator, skip
|
|
731
|
-
END CASE;
|
|
732
|
-
END LOOP;
|
|
733
|
-
END IF;
|
|
734
|
-
END LOOP;
|
|
735
|
-
|
|
736
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
737
|
-
EXECUTE format('
|
|
738
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
739
|
-
FROM (
|
|
740
|
-
SELECT * FROM acts_for
|
|
741
|
-
WHERE (TRUE) %s
|
|
742
|
-
ORDER BY %I %s
|
|
743
|
-
LIMIT %L OFFSET %L
|
|
744
|
-
) t
|
|
745
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
746
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
747
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
748
|
-
INTO v_results;
|
|
749
|
-
|
|
750
|
-
RETURN v_results;
|
|
751
|
-
END;
|
|
752
|
-
$$;
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
CREATE TABLE IF NOT EXISTS venues (
|
|
756
|
-
id serial PRIMARY KEY,
|
|
757
|
-
org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
758
|
-
name text UNIQUE NOT NULL,
|
|
759
|
-
address text NOT NULL,
|
|
760
|
-
description text
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_venues(p_user_id int, p_data jsonb)
|
|
765
|
-
RETURNS jsonb
|
|
766
|
-
LANGUAGE plpgsql
|
|
767
|
-
SECURITY DEFINER
|
|
768
|
-
SET search_path = dzql_v2, public
|
|
769
|
-
AS $$
|
|
770
|
-
DECLARE
|
|
771
|
-
v_result jsonb;
|
|
772
|
-
v_old_data jsonb;
|
|
773
|
-
v_commit_id bigint;
|
|
774
|
-
v_op text;
|
|
775
|
-
|
|
776
|
-
BEGIN
|
|
777
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
781
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM venues WHERE id = (p_data->>'id')::int) THEN
|
|
782
|
-
v_op := 'update';
|
|
783
|
-
|
|
784
|
-
-- Fetch old data for update rules/events
|
|
785
|
-
SELECT to_jsonb(venues.*) INTO v_old_data FROM venues WHERE id = (p_data->>'id')::int;
|
|
786
|
-
|
|
787
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
788
|
-
RAISE EXCEPTION 'permission_denied';
|
|
789
|
-
END IF;
|
|
790
|
-
|
|
791
|
-
-- Perform Partial Update
|
|
792
|
-
UPDATE venues SET
|
|
793
|
-
org_id = CASE WHEN (p_data ? 'org_id') THEN (p_data->>'org_id')::int ELSE org_id END,
|
|
794
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
795
|
-
address = CASE WHEN (p_data ? 'address') THEN (p_data->>'address') ELSE address END,
|
|
796
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END
|
|
797
|
-
WHERE id = (p_data->>'id')::int
|
|
798
|
-
RETURNING to_jsonb(venues.*) INTO v_result;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
ELSE
|
|
803
|
-
v_op := 'insert';
|
|
804
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
805
|
-
RAISE EXCEPTION 'permission_denied';
|
|
806
|
-
END IF;
|
|
807
|
-
|
|
808
|
-
-- Perform Insert
|
|
809
|
-
INSERT INTO venues (org_id, name, address, description)
|
|
810
|
-
VALUES ((p_data->>'org_id')::int, (p_data->>'name'), (p_data->>'address'), (p_data->>'description'))
|
|
811
|
-
RETURNING to_jsonb(venues.*) INTO v_result;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
END IF;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
-- Emit Event
|
|
819
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
820
|
-
VALUES (
|
|
821
|
-
v_commit_id,
|
|
822
|
-
'venues',
|
|
823
|
-
v_op,
|
|
824
|
-
jsonb_build_object('id', v_result->'id'),
|
|
825
|
-
v_result,
|
|
826
|
-
v_old_data, -- NULL for insert
|
|
827
|
-
p_user_id
|
|
828
|
-
);
|
|
829
|
-
|
|
830
|
-
-- Notify Runtime
|
|
831
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
832
|
-
|
|
833
|
-
-- Remove hidden fields before returning to client
|
|
834
|
-
RETURN v_result;
|
|
835
|
-
END;
|
|
836
|
-
$$;
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_venues(p_user_id int, p_pk jsonb)
|
|
840
|
-
RETURNS jsonb
|
|
841
|
-
LANGUAGE plpgsql
|
|
842
|
-
SECURITY DEFINER
|
|
843
|
-
SET search_path = dzql_v2, public
|
|
844
|
-
AS $$
|
|
845
|
-
DECLARE
|
|
846
|
-
v_old_data jsonb;
|
|
847
|
-
v_commit_id bigint;
|
|
848
|
-
BEGIN
|
|
849
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
850
|
-
|
|
851
|
-
-- Fetch old data FIRST for permission check
|
|
852
|
-
SELECT to_jsonb(venues.*) INTO v_old_data FROM venues WHERE id = (p_pk->>'id')::int;
|
|
853
|
-
|
|
854
|
-
IF v_old_data IS NULL THEN
|
|
855
|
-
RAISE EXCEPTION 'not_found';
|
|
856
|
-
END IF;
|
|
857
|
-
|
|
858
|
-
-- Permission Check (Delete)
|
|
859
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
860
|
-
RAISE EXCEPTION 'permission_denied';
|
|
861
|
-
END IF;
|
|
862
|
-
|
|
863
|
-
-- Graph Rules (Pre-delete cascades)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
-- Perform Delete
|
|
867
|
-
DELETE FROM venues WHERE id = (p_pk->>'id')::int;
|
|
868
|
-
|
|
869
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
870
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
871
|
-
VALUES (
|
|
872
|
-
v_commit_id,
|
|
873
|
-
'venues',
|
|
874
|
-
'delete',
|
|
875
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
876
|
-
v_old_data, -- Include full data for subscription resolution
|
|
877
|
-
v_old_data,
|
|
878
|
-
p_user_id
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
-- Notify Runtime
|
|
882
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
883
|
-
|
|
884
|
-
-- Remove hidden fields before returning to client
|
|
885
|
-
RETURN v_old_data;
|
|
886
|
-
END;
|
|
887
|
-
$$;
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_venues(p_user_id int, p_pk jsonb)
|
|
891
|
-
RETURNS jsonb
|
|
892
|
-
LANGUAGE plpgsql
|
|
893
|
-
SECURITY DEFINER
|
|
894
|
-
SET search_path = dzql_v2, public
|
|
895
|
-
AS $$
|
|
896
|
-
DECLARE
|
|
897
|
-
v_result jsonb;
|
|
898
|
-
BEGIN
|
|
899
|
-
SELECT to_jsonb(venues.*) INTO v_result
|
|
900
|
-
FROM venues
|
|
901
|
-
WHERE id = (p_pk->>'id')::int
|
|
902
|
-
AND (TRUE);
|
|
903
|
-
|
|
904
|
-
IF v_result IS NULL THEN
|
|
905
|
-
RETURN NULL;
|
|
906
|
-
END IF;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
RETURN v_result;
|
|
910
|
-
END;
|
|
911
|
-
$$;
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_venues(p_user_id int, p_query jsonb)
|
|
915
|
-
RETURNS jsonb
|
|
916
|
-
LANGUAGE plpgsql
|
|
917
|
-
SECURITY DEFINER
|
|
918
|
-
SET search_path = dzql_v2, public
|
|
919
|
-
AS $$
|
|
920
|
-
DECLARE
|
|
921
|
-
v_results jsonb;
|
|
922
|
-
v_filters jsonb;
|
|
923
|
-
v_sort_field text;
|
|
924
|
-
v_sort_order text;
|
|
925
|
-
v_where_clause text := '';
|
|
926
|
-
v_field text;
|
|
927
|
-
v_filter jsonb;
|
|
928
|
-
v_operator text;
|
|
929
|
-
v_value jsonb;
|
|
930
|
-
BEGIN
|
|
931
|
-
-- Extract query parameters
|
|
932
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
933
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
934
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
935
|
-
|
|
936
|
-
-- Build WHERE clause from filters
|
|
937
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
938
|
-
LOOP
|
|
939
|
-
-- Handle simple value (exact match)
|
|
940
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
941
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
942
|
-
ELSE
|
|
943
|
-
-- Handle operator-based filters
|
|
944
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
945
|
-
LOOP
|
|
946
|
-
CASE v_operator
|
|
947
|
-
WHEN 'eq' THEN
|
|
948
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
949
|
-
WHEN 'ne' THEN
|
|
950
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
951
|
-
WHEN 'gt' THEN
|
|
952
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
953
|
-
WHEN 'gte' THEN
|
|
954
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
955
|
-
WHEN 'lt' THEN
|
|
956
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
957
|
-
WHEN 'lte' THEN
|
|
958
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
959
|
-
WHEN 'in' THEN
|
|
960
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
961
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
962
|
-
WHEN 'not_in' THEN
|
|
963
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
964
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
965
|
-
WHEN 'like' THEN
|
|
966
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
967
|
-
WHEN 'ilike' THEN
|
|
968
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
969
|
-
WHEN 'is_null' THEN
|
|
970
|
-
IF (v_value::text = 'true') THEN
|
|
971
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
972
|
-
END IF;
|
|
973
|
-
WHEN 'not_null' THEN
|
|
974
|
-
IF (v_value::text = 'true') THEN
|
|
975
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
976
|
-
END IF;
|
|
977
|
-
ELSE
|
|
978
|
-
-- Unknown operator, skip
|
|
979
|
-
END CASE;
|
|
980
|
-
END LOOP;
|
|
981
|
-
END IF;
|
|
982
|
-
END LOOP;
|
|
983
|
-
|
|
984
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
985
|
-
EXECUTE format('
|
|
986
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
987
|
-
FROM (
|
|
988
|
-
SELECT * FROM venues
|
|
989
|
-
WHERE (TRUE) %s
|
|
990
|
-
ORDER BY %I %s
|
|
991
|
-
LIMIT %L OFFSET %L
|
|
992
|
-
) t
|
|
993
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
994
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
995
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
996
|
-
INTO v_results;
|
|
997
|
-
|
|
998
|
-
RETURN v_results;
|
|
999
|
-
END;
|
|
1000
|
-
$$;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
CREATE TABLE IF NOT EXISTS sites (
|
|
1004
|
-
id serial PRIMARY KEY,
|
|
1005
|
-
venue_id int NOT NULL REFERENCES venues(id),
|
|
1006
|
-
name text NOT NULL,
|
|
1007
|
-
description text
|
|
1008
|
-
);
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_sites(p_user_id int, p_data jsonb)
|
|
1012
|
-
RETURNS jsonb
|
|
1013
|
-
LANGUAGE plpgsql
|
|
1014
|
-
SECURITY DEFINER
|
|
1015
|
-
SET search_path = dzql_v2, public
|
|
1016
|
-
AS $$
|
|
1017
|
-
DECLARE
|
|
1018
|
-
v_result jsonb;
|
|
1019
|
-
v_old_data jsonb;
|
|
1020
|
-
v_commit_id bigint;
|
|
1021
|
-
v_op text;
|
|
1022
|
-
|
|
1023
|
-
BEGIN
|
|
1024
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
1028
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM sites WHERE id = (p_data->>'id')::int) THEN
|
|
1029
|
-
v_op := 'update';
|
|
1030
|
-
|
|
1031
|
-
-- Fetch old data for update rules/events
|
|
1032
|
-
SELECT to_jsonb(sites.*) INTO v_old_data FROM sites WHERE id = (p_data->>'id')::int;
|
|
1033
|
-
|
|
1034
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (p_data->>'venue_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1035
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1036
|
-
END IF;
|
|
1037
|
-
|
|
1038
|
-
-- Perform Partial Update
|
|
1039
|
-
UPDATE sites SET
|
|
1040
|
-
venue_id = CASE WHEN (p_data ? 'venue_id') THEN (p_data->>'venue_id')::int ELSE venue_id END,
|
|
1041
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
1042
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END
|
|
1043
|
-
WHERE id = (p_data->>'id')::int
|
|
1044
|
-
RETURNING to_jsonb(sites.*) INTO v_result;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
ELSE
|
|
1049
|
-
v_op := 'insert';
|
|
1050
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (p_data->>'venue_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1051
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1052
|
-
END IF;
|
|
1053
|
-
|
|
1054
|
-
-- Perform Insert
|
|
1055
|
-
INSERT INTO sites (venue_id, name, description)
|
|
1056
|
-
VALUES ((p_data->>'venue_id')::int, (p_data->>'name'), (p_data->>'description'))
|
|
1057
|
-
RETURNING to_jsonb(sites.*) INTO v_result;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
END IF;
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
-- Emit Event
|
|
1065
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1066
|
-
VALUES (
|
|
1067
|
-
v_commit_id,
|
|
1068
|
-
'sites',
|
|
1069
|
-
v_op,
|
|
1070
|
-
jsonb_build_object('id', v_result->'id'),
|
|
1071
|
-
v_result,
|
|
1072
|
-
v_old_data, -- NULL for insert
|
|
1073
|
-
p_user_id
|
|
1074
|
-
);
|
|
1075
|
-
|
|
1076
|
-
-- Notify Runtime
|
|
1077
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1078
|
-
|
|
1079
|
-
-- Remove hidden fields before returning to client
|
|
1080
|
-
RETURN v_result;
|
|
1081
|
-
END;
|
|
1082
|
-
$$;
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_sites(p_user_id int, p_pk jsonb)
|
|
1086
|
-
RETURNS jsonb
|
|
1087
|
-
LANGUAGE plpgsql
|
|
1088
|
-
SECURITY DEFINER
|
|
1089
|
-
SET search_path = dzql_v2, public
|
|
1090
|
-
AS $$
|
|
1091
|
-
DECLARE
|
|
1092
|
-
v_old_data jsonb;
|
|
1093
|
-
v_commit_id bigint;
|
|
1094
|
-
BEGIN
|
|
1095
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1096
|
-
|
|
1097
|
-
-- Fetch old data FIRST for permission check
|
|
1098
|
-
SELECT to_jsonb(sites.*) INTO v_old_data FROM sites WHERE id = (p_pk->>'id')::int;
|
|
1099
|
-
|
|
1100
|
-
IF v_old_data IS NULL THEN
|
|
1101
|
-
RAISE EXCEPTION 'not_found';
|
|
1102
|
-
END IF;
|
|
1103
|
-
|
|
1104
|
-
-- Permission Check (Delete)
|
|
1105
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (v_old_data->>'venue_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1106
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1107
|
-
END IF;
|
|
1108
|
-
|
|
1109
|
-
-- Graph Rules (Pre-delete cascades)
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
-- Perform Delete
|
|
1113
|
-
DELETE FROM sites WHERE id = (p_pk->>'id')::int;
|
|
1114
|
-
|
|
1115
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
1116
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1117
|
-
VALUES (
|
|
1118
|
-
v_commit_id,
|
|
1119
|
-
'sites',
|
|
1120
|
-
'delete',
|
|
1121
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
1122
|
-
v_old_data, -- Include full data for subscription resolution
|
|
1123
|
-
v_old_data,
|
|
1124
|
-
p_user_id
|
|
1125
|
-
);
|
|
1126
|
-
|
|
1127
|
-
-- Notify Runtime
|
|
1128
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1129
|
-
|
|
1130
|
-
-- Remove hidden fields before returning to client
|
|
1131
|
-
RETURN v_old_data;
|
|
1132
|
-
END;
|
|
1133
|
-
$$;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_sites(p_user_id int, p_pk jsonb)
|
|
1137
|
-
RETURNS jsonb
|
|
1138
|
-
LANGUAGE plpgsql
|
|
1139
|
-
SECURITY DEFINER
|
|
1140
|
-
SET search_path = dzql_v2, public
|
|
1141
|
-
AS $$
|
|
1142
|
-
DECLARE
|
|
1143
|
-
v_result jsonb;
|
|
1144
|
-
BEGIN
|
|
1145
|
-
SELECT to_jsonb(sites.*) INTO v_result
|
|
1146
|
-
FROM sites
|
|
1147
|
-
WHERE id = (p_pk->>'id')::int
|
|
1148
|
-
AND (TRUE);
|
|
1149
|
-
|
|
1150
|
-
IF v_result IS NULL THEN
|
|
1151
|
-
RETURN NULL;
|
|
1152
|
-
END IF;
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
RETURN v_result;
|
|
1156
|
-
END;
|
|
1157
|
-
$$;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_sites(p_user_id int, p_query jsonb)
|
|
1161
|
-
RETURNS jsonb
|
|
1162
|
-
LANGUAGE plpgsql
|
|
1163
|
-
SECURITY DEFINER
|
|
1164
|
-
SET search_path = dzql_v2, public
|
|
1165
|
-
AS $$
|
|
1166
|
-
DECLARE
|
|
1167
|
-
v_results jsonb;
|
|
1168
|
-
v_filters jsonb;
|
|
1169
|
-
v_sort_field text;
|
|
1170
|
-
v_sort_order text;
|
|
1171
|
-
v_where_clause text := '';
|
|
1172
|
-
v_field text;
|
|
1173
|
-
v_filter jsonb;
|
|
1174
|
-
v_operator text;
|
|
1175
|
-
v_value jsonb;
|
|
1176
|
-
BEGIN
|
|
1177
|
-
-- Extract query parameters
|
|
1178
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
1179
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
1180
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
1181
|
-
|
|
1182
|
-
-- Build WHERE clause from filters
|
|
1183
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
1184
|
-
LOOP
|
|
1185
|
-
-- Handle simple value (exact match)
|
|
1186
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
1187
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
1188
|
-
ELSE
|
|
1189
|
-
-- Handle operator-based filters
|
|
1190
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
1191
|
-
LOOP
|
|
1192
|
-
CASE v_operator
|
|
1193
|
-
WHEN 'eq' THEN
|
|
1194
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
1195
|
-
WHEN 'ne' THEN
|
|
1196
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
1197
|
-
WHEN 'gt' THEN
|
|
1198
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
1199
|
-
WHEN 'gte' THEN
|
|
1200
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
1201
|
-
WHEN 'lt' THEN
|
|
1202
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
1203
|
-
WHEN 'lte' THEN
|
|
1204
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
1205
|
-
WHEN 'in' THEN
|
|
1206
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
1207
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1208
|
-
WHEN 'not_in' THEN
|
|
1209
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
1210
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1211
|
-
WHEN 'like' THEN
|
|
1212
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
1213
|
-
WHEN 'ilike' THEN
|
|
1214
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
1215
|
-
WHEN 'is_null' THEN
|
|
1216
|
-
IF (v_value::text = 'true') THEN
|
|
1217
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
1218
|
-
END IF;
|
|
1219
|
-
WHEN 'not_null' THEN
|
|
1220
|
-
IF (v_value::text = 'true') THEN
|
|
1221
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
1222
|
-
END IF;
|
|
1223
|
-
ELSE
|
|
1224
|
-
-- Unknown operator, skip
|
|
1225
|
-
END CASE;
|
|
1226
|
-
END LOOP;
|
|
1227
|
-
END IF;
|
|
1228
|
-
END LOOP;
|
|
1229
|
-
|
|
1230
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
1231
|
-
EXECUTE format('
|
|
1232
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
1233
|
-
FROM (
|
|
1234
|
-
SELECT * FROM sites
|
|
1235
|
-
WHERE (TRUE) %s
|
|
1236
|
-
ORDER BY %I %s
|
|
1237
|
-
LIMIT %L OFFSET %L
|
|
1238
|
-
) t
|
|
1239
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
1240
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
1241
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
1242
|
-
INTO v_results;
|
|
1243
|
-
|
|
1244
|
-
RETURN v_results;
|
|
1245
|
-
END;
|
|
1246
|
-
$$;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
CREATE TABLE IF NOT EXISTS products (
|
|
1250
|
-
id serial PRIMARY KEY,
|
|
1251
|
-
org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
1252
|
-
name text UNIQUE NOT NULL,
|
|
1253
|
-
description text,
|
|
1254
|
-
price decimal(10, 2) NOT NULL DEFAULT 0.00,
|
|
1255
|
-
created_by int REFERENCES users(id),
|
|
1256
|
-
created_at timestamptz,
|
|
1257
|
-
deleted_at timestamptz
|
|
1258
|
-
);
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_products(p_user_id int, p_data jsonb)
|
|
1262
|
-
RETURNS jsonb
|
|
1263
|
-
LANGUAGE plpgsql
|
|
1264
|
-
SECURITY DEFINER
|
|
1265
|
-
SET search_path = dzql_v2, public
|
|
1266
|
-
AS $$
|
|
1267
|
-
DECLARE
|
|
1268
|
-
v_result jsonb;
|
|
1269
|
-
v_old_data jsonb;
|
|
1270
|
-
v_commit_id bigint;
|
|
1271
|
-
v_op text;
|
|
1272
|
-
|
|
1273
|
-
BEGIN
|
|
1274
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
1278
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM products WHERE id = (p_data->>'id')::int) THEN
|
|
1279
|
-
v_op := 'update';
|
|
1280
|
-
|
|
1281
|
-
-- Fetch old data for update rules/events
|
|
1282
|
-
SELECT to_jsonb(products.*) INTO v_old_data FROM products WHERE id = (p_data->>'id')::int;
|
|
1283
|
-
|
|
1284
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1285
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1286
|
-
END IF;
|
|
1287
|
-
|
|
1288
|
-
-- Perform Partial Update
|
|
1289
|
-
UPDATE products SET
|
|
1290
|
-
org_id = CASE WHEN (p_data ? 'org_id') THEN (p_data->>'org_id')::int ELSE org_id END,
|
|
1291
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
1292
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END,
|
|
1293
|
-
price = CASE WHEN (p_data ? 'price') THEN (p_data->>'price')::numeric ELSE price END,
|
|
1294
|
-
created_by = CASE WHEN (p_data ? 'created_by') THEN (p_data->>'created_by')::int ELSE created_by END,
|
|
1295
|
-
created_at = CASE WHEN (p_data ? 'created_at') THEN (p_data->>'created_at')::timestamptz ELSE created_at END,
|
|
1296
|
-
deleted_at = CASE WHEN (p_data ? 'deleted_at') THEN (p_data->>'deleted_at')::timestamptz ELSE deleted_at END
|
|
1297
|
-
WHERE id = (p_data->>'id')::int
|
|
1298
|
-
RETURNING to_jsonb(products.*) INTO v_result;
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
ELSE
|
|
1303
|
-
v_op := 'insert';
|
|
1304
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1305
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1306
|
-
END IF;
|
|
1307
|
-
|
|
1308
|
-
-- Perform Insert
|
|
1309
|
-
INSERT INTO products (org_id, name, description, price, created_by, created_at, deleted_at)
|
|
1310
|
-
VALUES ((p_data->>'org_id')::int, (p_data->>'name'), (p_data->>'description'), (p_data->>'price')::numeric, COALESCE((p_data->>'created_by')::int, p_user_id), COALESCE((p_data->>'created_at')::timestamptz, now()), (p_data->>'deleted_at')::timestamptz)
|
|
1311
|
-
RETURNING to_jsonb(products.*) INTO v_result;
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
END IF;
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
-- Emit Event
|
|
1319
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1320
|
-
VALUES (
|
|
1321
|
-
v_commit_id,
|
|
1322
|
-
'products',
|
|
1323
|
-
v_op,
|
|
1324
|
-
jsonb_build_object('id', v_result->'id'),
|
|
1325
|
-
v_result,
|
|
1326
|
-
v_old_data, -- NULL for insert
|
|
1327
|
-
p_user_id
|
|
1328
|
-
);
|
|
1329
|
-
|
|
1330
|
-
-- Notify Runtime
|
|
1331
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1332
|
-
|
|
1333
|
-
-- Remove hidden fields before returning to client
|
|
1334
|
-
RETURN v_result;
|
|
1335
|
-
END;
|
|
1336
|
-
$$;
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_products(p_user_id int, p_pk jsonb)
|
|
1340
|
-
RETURNS jsonb
|
|
1341
|
-
LANGUAGE plpgsql
|
|
1342
|
-
SECURITY DEFINER
|
|
1343
|
-
SET search_path = dzql_v2, public
|
|
1344
|
-
AS $$
|
|
1345
|
-
DECLARE
|
|
1346
|
-
v_old_data jsonb;
|
|
1347
|
-
v_commit_id bigint;
|
|
1348
|
-
BEGIN
|
|
1349
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1350
|
-
|
|
1351
|
-
-- Fetch old data FIRST for permission check
|
|
1352
|
-
SELECT to_jsonb(products.*) INTO v_old_data FROM products WHERE id = (p_pk->>'id')::int;
|
|
1353
|
-
|
|
1354
|
-
IF v_old_data IS NULL THEN
|
|
1355
|
-
RAISE EXCEPTION 'not_found';
|
|
1356
|
-
END IF;
|
|
1357
|
-
|
|
1358
|
-
-- Permission Check (Delete)
|
|
1359
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1360
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1361
|
-
END IF;
|
|
1362
|
-
|
|
1363
|
-
-- Graph Rules (Pre-delete cascades)
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
-- Perform Soft Delete
|
|
1367
|
-
UPDATE products SET deleted_at = now() WHERE id = (p_pk->>'id')::int;
|
|
1368
|
-
|
|
1369
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
1370
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1371
|
-
VALUES (
|
|
1372
|
-
v_commit_id,
|
|
1373
|
-
'products',
|
|
1374
|
-
'delete',
|
|
1375
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
1376
|
-
v_old_data, -- Include full data for subscription resolution
|
|
1377
|
-
v_old_data,
|
|
1378
|
-
p_user_id
|
|
1379
|
-
);
|
|
1380
|
-
|
|
1381
|
-
-- Notify Runtime
|
|
1382
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1383
|
-
|
|
1384
|
-
-- Remove hidden fields before returning to client
|
|
1385
|
-
RETURN v_old_data;
|
|
1386
|
-
END;
|
|
1387
|
-
$$;
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_products(p_user_id int, p_pk jsonb)
|
|
1391
|
-
RETURNS jsonb
|
|
1392
|
-
LANGUAGE plpgsql
|
|
1393
|
-
SECURITY DEFINER
|
|
1394
|
-
SET search_path = dzql_v2, public
|
|
1395
|
-
AS $$
|
|
1396
|
-
DECLARE
|
|
1397
|
-
v_result jsonb;
|
|
1398
|
-
BEGIN
|
|
1399
|
-
SELECT to_jsonb(products.*) INTO v_result
|
|
1400
|
-
FROM products
|
|
1401
|
-
WHERE id = (p_pk->>'id')::int
|
|
1402
|
-
AND (TRUE);
|
|
1403
|
-
|
|
1404
|
-
IF v_result IS NULL THEN
|
|
1405
|
-
RETURN NULL;
|
|
1406
|
-
END IF;
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
RETURN v_result;
|
|
1410
|
-
END;
|
|
1411
|
-
$$;
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_products(p_user_id int, p_query jsonb)
|
|
1415
|
-
RETURNS jsonb
|
|
1416
|
-
LANGUAGE plpgsql
|
|
1417
|
-
SECURITY DEFINER
|
|
1418
|
-
SET search_path = dzql_v2, public
|
|
1419
|
-
AS $$
|
|
1420
|
-
DECLARE
|
|
1421
|
-
v_results jsonb;
|
|
1422
|
-
v_filters jsonb;
|
|
1423
|
-
v_sort_field text;
|
|
1424
|
-
v_sort_order text;
|
|
1425
|
-
v_where_clause text := '';
|
|
1426
|
-
v_field text;
|
|
1427
|
-
v_filter jsonb;
|
|
1428
|
-
v_operator text;
|
|
1429
|
-
v_value jsonb;
|
|
1430
|
-
BEGIN
|
|
1431
|
-
-- Extract query parameters
|
|
1432
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
1433
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
1434
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
1435
|
-
|
|
1436
|
-
-- Build WHERE clause from filters
|
|
1437
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
1438
|
-
LOOP
|
|
1439
|
-
-- Handle simple value (exact match)
|
|
1440
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
1441
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
1442
|
-
ELSE
|
|
1443
|
-
-- Handle operator-based filters
|
|
1444
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
1445
|
-
LOOP
|
|
1446
|
-
CASE v_operator
|
|
1447
|
-
WHEN 'eq' THEN
|
|
1448
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
1449
|
-
WHEN 'ne' THEN
|
|
1450
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
1451
|
-
WHEN 'gt' THEN
|
|
1452
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
1453
|
-
WHEN 'gte' THEN
|
|
1454
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
1455
|
-
WHEN 'lt' THEN
|
|
1456
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
1457
|
-
WHEN 'lte' THEN
|
|
1458
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
1459
|
-
WHEN 'in' THEN
|
|
1460
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
1461
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1462
|
-
WHEN 'not_in' THEN
|
|
1463
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
1464
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1465
|
-
WHEN 'like' THEN
|
|
1466
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
1467
|
-
WHEN 'ilike' THEN
|
|
1468
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
1469
|
-
WHEN 'is_null' THEN
|
|
1470
|
-
IF (v_value::text = 'true') THEN
|
|
1471
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
1472
|
-
END IF;
|
|
1473
|
-
WHEN 'not_null' THEN
|
|
1474
|
-
IF (v_value::text = 'true') THEN
|
|
1475
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
1476
|
-
END IF;
|
|
1477
|
-
ELSE
|
|
1478
|
-
-- Unknown operator, skip
|
|
1479
|
-
END CASE;
|
|
1480
|
-
END LOOP;
|
|
1481
|
-
END IF;
|
|
1482
|
-
END LOOP;
|
|
1483
|
-
|
|
1484
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
1485
|
-
EXECUTE format('
|
|
1486
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
1487
|
-
FROM (
|
|
1488
|
-
SELECT * FROM products
|
|
1489
|
-
WHERE (TRUE) AND deleted_at IS NULL %s
|
|
1490
|
-
ORDER BY %I %s
|
|
1491
|
-
LIMIT %L OFFSET %L
|
|
1492
|
-
) t
|
|
1493
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
1494
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
1495
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
1496
|
-
INTO v_results;
|
|
1497
|
-
|
|
1498
|
-
RETURN v_results;
|
|
1499
|
-
END;
|
|
1500
|
-
$$;
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
CREATE TABLE IF NOT EXISTS packages (
|
|
1504
|
-
id serial PRIMARY KEY,
|
|
1505
|
-
owner_org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
1506
|
-
sponsor_org_id int REFERENCES organisations(id) ON DELETE SET NULL,
|
|
1507
|
-
name text NOT NULL,
|
|
1508
|
-
price decimal(10, 2) NOT NULL DEFAULT 0.00,
|
|
1509
|
-
status text NOT NULL DEFAULT 'draft'
|
|
1510
|
-
);
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_packages(p_user_id int, p_data jsonb)
|
|
1514
|
-
RETURNS jsonb
|
|
1515
|
-
LANGUAGE plpgsql
|
|
1516
|
-
SECURITY DEFINER
|
|
1517
|
-
SET search_path = dzql_v2, public
|
|
1518
|
-
AS $$
|
|
1519
|
-
DECLARE
|
|
1520
|
-
v_result jsonb;
|
|
1521
|
-
v_old_data jsonb;
|
|
1522
|
-
v_commit_id bigint;
|
|
1523
|
-
v_op text;
|
|
1524
|
-
|
|
1525
|
-
BEGIN
|
|
1526
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
1530
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM packages WHERE id = (p_data->>'id')::int) THEN
|
|
1531
|
-
v_op := 'update';
|
|
1532
|
-
|
|
1533
|
-
-- Fetch old data for update rules/events
|
|
1534
|
-
SELECT to_jsonb(packages.*) INTO v_old_data FROM packages WHERE id = (p_data->>'id')::int;
|
|
1535
|
-
|
|
1536
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'owner_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1537
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1538
|
-
END IF;
|
|
1539
|
-
|
|
1540
|
-
-- Perform Partial Update
|
|
1541
|
-
UPDATE packages SET
|
|
1542
|
-
owner_org_id = CASE WHEN (p_data ? 'owner_org_id') THEN (p_data->>'owner_org_id')::int ELSE owner_org_id END,
|
|
1543
|
-
sponsor_org_id = CASE WHEN (p_data ? 'sponsor_org_id') THEN (p_data->>'sponsor_org_id')::int ELSE sponsor_org_id END,
|
|
1544
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
1545
|
-
price = CASE WHEN (p_data ? 'price') THEN (p_data->>'price')::numeric ELSE price END,
|
|
1546
|
-
status = CASE WHEN (p_data ? 'status') THEN (p_data->>'status') ELSE status END
|
|
1547
|
-
WHERE id = (p_data->>'id')::int
|
|
1548
|
-
RETURNING to_jsonb(packages.*) INTO v_result;
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
ELSE
|
|
1553
|
-
v_op := 'insert';
|
|
1554
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'owner_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1555
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1556
|
-
END IF;
|
|
1557
|
-
|
|
1558
|
-
-- Perform Insert
|
|
1559
|
-
INSERT INTO packages (owner_org_id, sponsor_org_id, name, price, status)
|
|
1560
|
-
VALUES ((p_data->>'owner_org_id')::int, (p_data->>'sponsor_org_id')::int, (p_data->>'name'), (p_data->>'price')::numeric, (p_data->>'status'))
|
|
1561
|
-
RETURNING to_jsonb(packages.*) INTO v_result;
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
END IF;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
-- Emit Event
|
|
1569
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1570
|
-
VALUES (
|
|
1571
|
-
v_commit_id,
|
|
1572
|
-
'packages',
|
|
1573
|
-
v_op,
|
|
1574
|
-
jsonb_build_object('id', v_result->'id'),
|
|
1575
|
-
v_result,
|
|
1576
|
-
v_old_data, -- NULL for insert
|
|
1577
|
-
p_user_id
|
|
1578
|
-
);
|
|
1579
|
-
|
|
1580
|
-
-- Notify Runtime
|
|
1581
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1582
|
-
|
|
1583
|
-
-- Remove hidden fields before returning to client
|
|
1584
|
-
RETURN v_result;
|
|
1585
|
-
END;
|
|
1586
|
-
$$;
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_packages(p_user_id int, p_pk jsonb)
|
|
1590
|
-
RETURNS jsonb
|
|
1591
|
-
LANGUAGE plpgsql
|
|
1592
|
-
SECURITY DEFINER
|
|
1593
|
-
SET search_path = dzql_v2, public
|
|
1594
|
-
AS $$
|
|
1595
|
-
DECLARE
|
|
1596
|
-
v_old_data jsonb;
|
|
1597
|
-
v_commit_id bigint;
|
|
1598
|
-
BEGIN
|
|
1599
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1600
|
-
|
|
1601
|
-
-- Fetch old data FIRST for permission check
|
|
1602
|
-
SELECT to_jsonb(packages.*) INTO v_old_data FROM packages WHERE id = (p_pk->>'id')::int;
|
|
1603
|
-
|
|
1604
|
-
IF v_old_data IS NULL THEN
|
|
1605
|
-
RAISE EXCEPTION 'not_found';
|
|
1606
|
-
END IF;
|
|
1607
|
-
|
|
1608
|
-
-- Permission Check (Delete)
|
|
1609
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'owner_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1610
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1611
|
-
END IF;
|
|
1612
|
-
|
|
1613
|
-
-- Graph Rules (Pre-delete cascades)
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
-- Perform Delete
|
|
1617
|
-
DELETE FROM packages WHERE id = (p_pk->>'id')::int;
|
|
1618
|
-
|
|
1619
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
1620
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1621
|
-
VALUES (
|
|
1622
|
-
v_commit_id,
|
|
1623
|
-
'packages',
|
|
1624
|
-
'delete',
|
|
1625
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
1626
|
-
v_old_data, -- Include full data for subscription resolution
|
|
1627
|
-
v_old_data,
|
|
1628
|
-
p_user_id
|
|
1629
|
-
);
|
|
1630
|
-
|
|
1631
|
-
-- Notify Runtime
|
|
1632
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1633
|
-
|
|
1634
|
-
-- Remove hidden fields before returning to client
|
|
1635
|
-
RETURN v_old_data;
|
|
1636
|
-
END;
|
|
1637
|
-
$$;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_packages(p_user_id int, p_pk jsonb)
|
|
1641
|
-
RETURNS jsonb
|
|
1642
|
-
LANGUAGE plpgsql
|
|
1643
|
-
SECURITY DEFINER
|
|
1644
|
-
SET search_path = dzql_v2, public
|
|
1645
|
-
AS $$
|
|
1646
|
-
DECLARE
|
|
1647
|
-
v_result jsonb;
|
|
1648
|
-
BEGIN
|
|
1649
|
-
SELECT to_jsonb(packages.*) INTO v_result
|
|
1650
|
-
FROM packages
|
|
1651
|
-
WHERE id = (p_pk->>'id')::int
|
|
1652
|
-
AND (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = packages.owner_org_id AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = packages.sponsor_org_id AND acts_for.active = true AND acts_for.user_id = p_user_id));
|
|
1653
|
-
|
|
1654
|
-
IF v_result IS NULL THEN
|
|
1655
|
-
RETURN NULL;
|
|
1656
|
-
END IF;
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
RETURN v_result;
|
|
1660
|
-
END;
|
|
1661
|
-
$$;
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_packages(p_user_id int, p_query jsonb)
|
|
1665
|
-
RETURNS jsonb
|
|
1666
|
-
LANGUAGE plpgsql
|
|
1667
|
-
SECURITY DEFINER
|
|
1668
|
-
SET search_path = dzql_v2, public
|
|
1669
|
-
AS $$
|
|
1670
|
-
DECLARE
|
|
1671
|
-
v_results jsonb;
|
|
1672
|
-
v_filters jsonb;
|
|
1673
|
-
v_sort_field text;
|
|
1674
|
-
v_sort_order text;
|
|
1675
|
-
v_where_clause text := '';
|
|
1676
|
-
v_field text;
|
|
1677
|
-
v_filter jsonb;
|
|
1678
|
-
v_operator text;
|
|
1679
|
-
v_value jsonb;
|
|
1680
|
-
BEGIN
|
|
1681
|
-
-- Extract query parameters
|
|
1682
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
1683
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
1684
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
1685
|
-
|
|
1686
|
-
-- Build WHERE clause from filters
|
|
1687
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
1688
|
-
LOOP
|
|
1689
|
-
-- Handle simple value (exact match)
|
|
1690
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
1691
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
1692
|
-
ELSE
|
|
1693
|
-
-- Handle operator-based filters
|
|
1694
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
1695
|
-
LOOP
|
|
1696
|
-
CASE v_operator
|
|
1697
|
-
WHEN 'eq' THEN
|
|
1698
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
1699
|
-
WHEN 'ne' THEN
|
|
1700
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
1701
|
-
WHEN 'gt' THEN
|
|
1702
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
1703
|
-
WHEN 'gte' THEN
|
|
1704
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
1705
|
-
WHEN 'lt' THEN
|
|
1706
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
1707
|
-
WHEN 'lte' THEN
|
|
1708
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
1709
|
-
WHEN 'in' THEN
|
|
1710
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
1711
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1712
|
-
WHEN 'not_in' THEN
|
|
1713
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
1714
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1715
|
-
WHEN 'like' THEN
|
|
1716
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
1717
|
-
WHEN 'ilike' THEN
|
|
1718
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
1719
|
-
WHEN 'is_null' THEN
|
|
1720
|
-
IF (v_value::text = 'true') THEN
|
|
1721
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
1722
|
-
END IF;
|
|
1723
|
-
WHEN 'not_null' THEN
|
|
1724
|
-
IF (v_value::text = 'true') THEN
|
|
1725
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
1726
|
-
END IF;
|
|
1727
|
-
ELSE
|
|
1728
|
-
-- Unknown operator, skip
|
|
1729
|
-
END CASE;
|
|
1730
|
-
END LOOP;
|
|
1731
|
-
END IF;
|
|
1732
|
-
END LOOP;
|
|
1733
|
-
|
|
1734
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
1735
|
-
EXECUTE format('
|
|
1736
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
1737
|
-
FROM (
|
|
1738
|
-
SELECT * FROM packages
|
|
1739
|
-
WHERE (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = packages.owner_org_id AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = packages.sponsor_org_id AND acts_for.active = true AND acts_for.user_id = p_user_id)) %s
|
|
1740
|
-
ORDER BY %I %s
|
|
1741
|
-
LIMIT %L OFFSET %L
|
|
1742
|
-
) t
|
|
1743
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
1744
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
1745
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
1746
|
-
INTO v_results;
|
|
1747
|
-
|
|
1748
|
-
RETURN v_results;
|
|
1749
|
-
END;
|
|
1750
|
-
$$;
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
CREATE TABLE IF NOT EXISTS allocations (
|
|
1754
|
-
id serial PRIMARY KEY,
|
|
1755
|
-
package_id int NOT NULL REFERENCES packages(id),
|
|
1756
|
-
site_id int NOT NULL REFERENCES sites(id),
|
|
1757
|
-
from_date date NOT NULL,
|
|
1758
|
-
to_date date NOT NULL
|
|
1759
|
-
);
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_allocations(p_user_id int, p_data jsonb)
|
|
1763
|
-
RETURNS jsonb
|
|
1764
|
-
LANGUAGE plpgsql
|
|
1765
|
-
SECURITY DEFINER
|
|
1766
|
-
SET search_path = dzql_v2, public
|
|
1767
|
-
AS $$
|
|
1768
|
-
DECLARE
|
|
1769
|
-
v_result jsonb;
|
|
1770
|
-
v_old_data jsonb;
|
|
1771
|
-
v_commit_id bigint;
|
|
1772
|
-
v_op text;
|
|
1773
|
-
|
|
1774
|
-
BEGIN
|
|
1775
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
1779
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM allocations WHERE id = (p_data->>'id')::int) THEN
|
|
1780
|
-
v_op := 'update';
|
|
1781
|
-
|
|
1782
|
-
-- Fetch old data for update rules/events
|
|
1783
|
-
SELECT to_jsonb(allocations.*) INTO v_old_data FROM allocations WHERE id = (p_data->>'id')::int;
|
|
1784
|
-
|
|
1785
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (SELECT venue_id FROM sites WHERE id = (p_data->>'site_id')::int)) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1786
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1787
|
-
END IF;
|
|
1788
|
-
|
|
1789
|
-
-- Perform Partial Update
|
|
1790
|
-
UPDATE allocations SET
|
|
1791
|
-
package_id = CASE WHEN (p_data ? 'package_id') THEN (p_data->>'package_id')::int ELSE package_id END,
|
|
1792
|
-
site_id = CASE WHEN (p_data ? 'site_id') THEN (p_data->>'site_id')::int ELSE site_id END,
|
|
1793
|
-
from_date = CASE WHEN (p_data ? 'from_date') THEN (p_data->>'from_date')::date ELSE from_date END,
|
|
1794
|
-
to_date = CASE WHEN (p_data ? 'to_date') THEN (p_data->>'to_date')::date ELSE to_date END
|
|
1795
|
-
WHERE id = (p_data->>'id')::int
|
|
1796
|
-
RETURNING to_jsonb(allocations.*) INTO v_result;
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
ELSE
|
|
1801
|
-
v_op := 'insert';
|
|
1802
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (SELECT venue_id FROM sites WHERE id = (p_data->>'site_id')::int)) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1803
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1804
|
-
END IF;
|
|
1805
|
-
|
|
1806
|
-
-- Perform Insert
|
|
1807
|
-
INSERT INTO allocations (package_id, site_id, from_date, to_date)
|
|
1808
|
-
VALUES ((p_data->>'package_id')::int, (p_data->>'site_id')::int, (p_data->>'from_date')::date, (p_data->>'to_date')::date)
|
|
1809
|
-
RETURNING to_jsonb(allocations.*) INTO v_result;
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
END IF;
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
-- Emit Event
|
|
1817
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1818
|
-
VALUES (
|
|
1819
|
-
v_commit_id,
|
|
1820
|
-
'allocations',
|
|
1821
|
-
v_op,
|
|
1822
|
-
jsonb_build_object('id', v_result->'id'),
|
|
1823
|
-
v_result,
|
|
1824
|
-
v_old_data, -- NULL for insert
|
|
1825
|
-
p_user_id
|
|
1826
|
-
);
|
|
1827
|
-
|
|
1828
|
-
-- Notify Runtime
|
|
1829
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1830
|
-
|
|
1831
|
-
-- Remove hidden fields before returning to client
|
|
1832
|
-
RETURN v_result;
|
|
1833
|
-
END;
|
|
1834
|
-
$$;
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_allocations(p_user_id int, p_pk jsonb)
|
|
1838
|
-
RETURNS jsonb
|
|
1839
|
-
LANGUAGE plpgsql
|
|
1840
|
-
SECURITY DEFINER
|
|
1841
|
-
SET search_path = dzql_v2, public
|
|
1842
|
-
AS $$
|
|
1843
|
-
DECLARE
|
|
1844
|
-
v_old_data jsonb;
|
|
1845
|
-
v_commit_id bigint;
|
|
1846
|
-
BEGIN
|
|
1847
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
1848
|
-
|
|
1849
|
-
-- Fetch old data FIRST for permission check
|
|
1850
|
-
SELECT to_jsonb(allocations.*) INTO v_old_data FROM allocations WHERE id = (p_pk->>'id')::int;
|
|
1851
|
-
|
|
1852
|
-
IF v_old_data IS NULL THEN
|
|
1853
|
-
RAISE EXCEPTION 'not_found';
|
|
1854
|
-
END IF;
|
|
1855
|
-
|
|
1856
|
-
-- Permission Check (Delete)
|
|
1857
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (SELECT venue_id FROM sites WHERE id = (v_old_data->>'site_id')::int)) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
1858
|
-
RAISE EXCEPTION 'permission_denied';
|
|
1859
|
-
END IF;
|
|
1860
|
-
|
|
1861
|
-
-- Graph Rules (Pre-delete cascades)
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
-- Perform Delete
|
|
1865
|
-
DELETE FROM allocations WHERE id = (p_pk->>'id')::int;
|
|
1866
|
-
|
|
1867
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
1868
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
1869
|
-
VALUES (
|
|
1870
|
-
v_commit_id,
|
|
1871
|
-
'allocations',
|
|
1872
|
-
'delete',
|
|
1873
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
1874
|
-
v_old_data, -- Include full data for subscription resolution
|
|
1875
|
-
v_old_data,
|
|
1876
|
-
p_user_id
|
|
1877
|
-
);
|
|
1878
|
-
|
|
1879
|
-
-- Notify Runtime
|
|
1880
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
1881
|
-
|
|
1882
|
-
-- Remove hidden fields before returning to client
|
|
1883
|
-
RETURN v_old_data;
|
|
1884
|
-
END;
|
|
1885
|
-
$$;
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_allocations(p_user_id int, p_pk jsonb)
|
|
1889
|
-
RETURNS jsonb
|
|
1890
|
-
LANGUAGE plpgsql
|
|
1891
|
-
SECURITY DEFINER
|
|
1892
|
-
SET search_path = dzql_v2, public
|
|
1893
|
-
AS $$
|
|
1894
|
-
DECLARE
|
|
1895
|
-
v_result jsonb;
|
|
1896
|
-
BEGIN
|
|
1897
|
-
SELECT to_jsonb(allocations.*) INTO v_result
|
|
1898
|
-
FROM allocations
|
|
1899
|
-
WHERE id = (p_pk->>'id')::int
|
|
1900
|
-
AND (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (SELECT venue_id FROM sites WHERE id = allocations.site_id)) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT owner_org_id FROM packages WHERE id = allocations.package_id) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT sponsor_org_id FROM packages WHERE id = allocations.package_id) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT contractor_org_id FROM contractor_rights WHERE contractor_rights.package_id = allocations.package_id AND contractor_rights.active = true) AND acts_for.active = true AND acts_for.user_id = p_user_id));
|
|
1901
|
-
|
|
1902
|
-
IF v_result IS NULL THEN
|
|
1903
|
-
RETURN NULL;
|
|
1904
|
-
END IF;
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
RETURN v_result;
|
|
1908
|
-
END;
|
|
1909
|
-
$$;
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_allocations(p_user_id int, p_query jsonb)
|
|
1913
|
-
RETURNS jsonb
|
|
1914
|
-
LANGUAGE plpgsql
|
|
1915
|
-
SECURITY DEFINER
|
|
1916
|
-
SET search_path = dzql_v2, public
|
|
1917
|
-
AS $$
|
|
1918
|
-
DECLARE
|
|
1919
|
-
v_results jsonb;
|
|
1920
|
-
v_filters jsonb;
|
|
1921
|
-
v_sort_field text;
|
|
1922
|
-
v_sort_order text;
|
|
1923
|
-
v_where_clause text := '';
|
|
1924
|
-
v_field text;
|
|
1925
|
-
v_filter jsonb;
|
|
1926
|
-
v_operator text;
|
|
1927
|
-
v_value jsonb;
|
|
1928
|
-
BEGIN
|
|
1929
|
-
-- Extract query parameters
|
|
1930
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
1931
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'id');
|
|
1932
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
1933
|
-
|
|
1934
|
-
-- Build WHERE clause from filters
|
|
1935
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
1936
|
-
LOOP
|
|
1937
|
-
-- Handle simple value (exact match)
|
|
1938
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
1939
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
1940
|
-
ELSE
|
|
1941
|
-
-- Handle operator-based filters
|
|
1942
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
1943
|
-
LOOP
|
|
1944
|
-
CASE v_operator
|
|
1945
|
-
WHEN 'eq' THEN
|
|
1946
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
1947
|
-
WHEN 'ne' THEN
|
|
1948
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
1949
|
-
WHEN 'gt' THEN
|
|
1950
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
1951
|
-
WHEN 'gte' THEN
|
|
1952
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
1953
|
-
WHEN 'lt' THEN
|
|
1954
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
1955
|
-
WHEN 'lte' THEN
|
|
1956
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
1957
|
-
WHEN 'in' THEN
|
|
1958
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
1959
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1960
|
-
WHEN 'not_in' THEN
|
|
1961
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
1962
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
1963
|
-
WHEN 'like' THEN
|
|
1964
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
1965
|
-
WHEN 'ilike' THEN
|
|
1966
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
1967
|
-
WHEN 'is_null' THEN
|
|
1968
|
-
IF (v_value::text = 'true') THEN
|
|
1969
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
1970
|
-
END IF;
|
|
1971
|
-
WHEN 'not_null' THEN
|
|
1972
|
-
IF (v_value::text = 'true') THEN
|
|
1973
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
1974
|
-
END IF;
|
|
1975
|
-
ELSE
|
|
1976
|
-
-- Unknown operator, skip
|
|
1977
|
-
END CASE;
|
|
1978
|
-
END LOOP;
|
|
1979
|
-
END IF;
|
|
1980
|
-
END LOOP;
|
|
1981
|
-
|
|
1982
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
1983
|
-
EXECUTE format('
|
|
1984
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
1985
|
-
FROM (
|
|
1986
|
-
SELECT * FROM allocations
|
|
1987
|
-
WHERE (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM venues WHERE id = (SELECT venue_id FROM sites WHERE id = allocations.site_id)) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT owner_org_id FROM packages WHERE id = allocations.package_id) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT sponsor_org_id FROM packages WHERE id = allocations.package_id) AND acts_for.active = true AND acts_for.user_id = p_user_id) OR EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT contractor_org_id FROM contractor_rights WHERE contractor_rights.package_id = allocations.package_id AND contractor_rights.active = true) AND acts_for.active = true AND acts_for.user_id = p_user_id)) %s
|
|
1988
|
-
ORDER BY %I %s
|
|
1989
|
-
LIMIT %L OFFSET %L
|
|
1990
|
-
) t
|
|
1991
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
1992
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
1993
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
1994
|
-
INTO v_results;
|
|
1995
|
-
|
|
1996
|
-
RETURN v_results;
|
|
1997
|
-
END;
|
|
1998
|
-
$$;
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
CREATE TABLE IF NOT EXISTS contractor_rights (
|
|
2002
|
-
contractor_org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
2003
|
-
sponsor_org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
2004
|
-
package_id int NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
|
2005
|
-
valid_from date NOT NULL DEFAULT current_date,
|
|
2006
|
-
valid_to date
|
|
2007
|
-
);
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_contractor_rights(p_user_id int, p_data jsonb)
|
|
2011
|
-
RETURNS jsonb
|
|
2012
|
-
LANGUAGE plpgsql
|
|
2013
|
-
SECURITY DEFINER
|
|
2014
|
-
SET search_path = dzql_v2, public
|
|
2015
|
-
AS $$
|
|
2016
|
-
DECLARE
|
|
2017
|
-
v_result jsonb;
|
|
2018
|
-
v_old_data jsonb;
|
|
2019
|
-
v_commit_id bigint;
|
|
2020
|
-
v_op text;
|
|
2021
|
-
|
|
2022
|
-
BEGIN
|
|
2023
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
2027
|
-
IF ((p_data->>'contractor_org_id') IS NOT NULL AND (p_data->>'package_id') IS NOT NULL AND (p_data->>'valid_from') IS NOT NULL) AND EXISTS(SELECT 1 FROM contractor_rights WHERE contractor_org_id = (p_data->>'contractor_org_id')::int AND package_id = (p_data->>'package_id')::int AND valid_from = (p_data->>'valid_from')::date) THEN
|
|
2028
|
-
v_op := 'update';
|
|
2029
|
-
|
|
2030
|
-
-- Fetch old data for update rules/events
|
|
2031
|
-
SELECT to_jsonb(contractor_rights.*) INTO v_old_data FROM contractor_rights WHERE contractor_org_id = (p_data->>'contractor_org_id')::int AND package_id = (p_data->>'package_id')::int AND valid_from = (p_data->>'valid_from')::date;
|
|
2032
|
-
|
|
2033
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'sponsor_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2034
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2035
|
-
END IF;
|
|
2036
|
-
|
|
2037
|
-
-- Perform Partial Update
|
|
2038
|
-
UPDATE contractor_rights SET
|
|
2039
|
-
sponsor_org_id = CASE WHEN (p_data ? 'sponsor_org_id') THEN (p_data->>'sponsor_org_id')::int ELSE sponsor_org_id END,
|
|
2040
|
-
valid_to = CASE WHEN (p_data ? 'valid_to') THEN (p_data->>'valid_to')::date ELSE valid_to END
|
|
2041
|
-
WHERE contractor_org_id = (p_data->>'contractor_org_id')::int AND package_id = (p_data->>'package_id')::int AND valid_from = (p_data->>'valid_from')::date
|
|
2042
|
-
RETURNING to_jsonb(contractor_rights.*) INTO v_result;
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
ELSE
|
|
2047
|
-
v_op := 'insert';
|
|
2048
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'sponsor_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2049
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2050
|
-
END IF;
|
|
2051
|
-
|
|
2052
|
-
-- Perform Insert
|
|
2053
|
-
INSERT INTO contractor_rights (contractor_org_id, sponsor_org_id, package_id, valid_from, valid_to)
|
|
2054
|
-
VALUES ((p_data->>'contractor_org_id')::int, (p_data->>'sponsor_org_id')::int, (p_data->>'package_id')::int, (p_data->>'valid_from')::date, (p_data->>'valid_to')::date)
|
|
2055
|
-
RETURNING to_jsonb(contractor_rights.*) INTO v_result;
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
END IF;
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
-- Emit Event
|
|
2063
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2064
|
-
VALUES (
|
|
2065
|
-
v_commit_id,
|
|
2066
|
-
'contractor_rights',
|
|
2067
|
-
v_op,
|
|
2068
|
-
jsonb_build_object('contractor_org_id', v_result->'contractor_org_id', 'package_id', v_result->'package_id', 'valid_from', v_result->'valid_from'),
|
|
2069
|
-
v_result,
|
|
2070
|
-
v_old_data, -- NULL for insert
|
|
2071
|
-
p_user_id
|
|
2072
|
-
);
|
|
2073
|
-
|
|
2074
|
-
-- Notify Runtime
|
|
2075
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2076
|
-
|
|
2077
|
-
-- Remove hidden fields before returning to client
|
|
2078
|
-
RETURN v_result;
|
|
2079
|
-
END;
|
|
2080
|
-
$$;
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_contractor_rights(p_user_id int, p_pk jsonb)
|
|
2084
|
-
RETURNS jsonb
|
|
2085
|
-
LANGUAGE plpgsql
|
|
2086
|
-
SECURITY DEFINER
|
|
2087
|
-
SET search_path = dzql_v2, public
|
|
2088
|
-
AS $$
|
|
2089
|
-
DECLARE
|
|
2090
|
-
v_old_data jsonb;
|
|
2091
|
-
v_commit_id bigint;
|
|
2092
|
-
BEGIN
|
|
2093
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2094
|
-
|
|
2095
|
-
-- Fetch old data FIRST for permission check
|
|
2096
|
-
SELECT to_jsonb(contractor_rights.*) INTO v_old_data FROM contractor_rights WHERE contractor_org_id = (p_pk->>'contractor_org_id')::int AND package_id = (p_pk->>'package_id')::int AND valid_from = (p_pk->>'valid_from')::date;
|
|
2097
|
-
|
|
2098
|
-
IF v_old_data IS NULL THEN
|
|
2099
|
-
RAISE EXCEPTION 'not_found';
|
|
2100
|
-
END IF;
|
|
2101
|
-
|
|
2102
|
-
-- Permission Check (Delete)
|
|
2103
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'sponsor_org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2104
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2105
|
-
END IF;
|
|
2106
|
-
|
|
2107
|
-
-- Graph Rules (Pre-delete cascades)
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
-- Perform Delete
|
|
2111
|
-
DELETE FROM contractor_rights WHERE contractor_org_id = (p_pk->>'contractor_org_id')::int AND package_id = (p_pk->>'package_id')::int AND valid_from = (p_pk->>'valid_from')::date;
|
|
2112
|
-
|
|
2113
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
2114
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2115
|
-
VALUES (
|
|
2116
|
-
v_commit_id,
|
|
2117
|
-
'contractor_rights',
|
|
2118
|
-
'delete',
|
|
2119
|
-
jsonb_build_object('contractor_org_id', v_old_data->'contractor_org_id', 'package_id', v_old_data->'package_id', 'valid_from', v_old_data->'valid_from'),
|
|
2120
|
-
v_old_data, -- Include full data for subscription resolution
|
|
2121
|
-
v_old_data,
|
|
2122
|
-
p_user_id
|
|
2123
|
-
);
|
|
2124
|
-
|
|
2125
|
-
-- Notify Runtime
|
|
2126
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2127
|
-
|
|
2128
|
-
-- Remove hidden fields before returning to client
|
|
2129
|
-
RETURN v_old_data;
|
|
2130
|
-
END;
|
|
2131
|
-
$$;
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_contractor_rights(p_user_id int, p_pk jsonb)
|
|
2135
|
-
RETURNS jsonb
|
|
2136
|
-
LANGUAGE plpgsql
|
|
2137
|
-
SECURITY DEFINER
|
|
2138
|
-
SET search_path = dzql_v2, public
|
|
2139
|
-
AS $$
|
|
2140
|
-
DECLARE
|
|
2141
|
-
v_result jsonb;
|
|
2142
|
-
BEGIN
|
|
2143
|
-
SELECT to_jsonb(contractor_rights.*) INTO v_result
|
|
2144
|
-
FROM contractor_rights
|
|
2145
|
-
WHERE contractor_org_id = (p_pk->>'contractor_org_id')::int AND package_id = (p_pk->>'package_id')::int AND valid_from = (p_pk->>'valid_from')::date
|
|
2146
|
-
AND (TRUE);
|
|
2147
|
-
|
|
2148
|
-
IF v_result IS NULL THEN
|
|
2149
|
-
RETURN NULL;
|
|
2150
|
-
END IF;
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
RETURN v_result;
|
|
2154
|
-
END;
|
|
2155
|
-
$$;
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_contractor_rights(p_user_id int, p_query jsonb)
|
|
2159
|
-
RETURNS jsonb
|
|
2160
|
-
LANGUAGE plpgsql
|
|
2161
|
-
SECURITY DEFINER
|
|
2162
|
-
SET search_path = dzql_v2, public
|
|
2163
|
-
AS $$
|
|
2164
|
-
DECLARE
|
|
2165
|
-
v_results jsonb;
|
|
2166
|
-
v_filters jsonb;
|
|
2167
|
-
v_sort_field text;
|
|
2168
|
-
v_sort_order text;
|
|
2169
|
-
v_where_clause text := '';
|
|
2170
|
-
v_field text;
|
|
2171
|
-
v_filter jsonb;
|
|
2172
|
-
v_operator text;
|
|
2173
|
-
v_value jsonb;
|
|
2174
|
-
BEGIN
|
|
2175
|
-
-- Extract query parameters
|
|
2176
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
2177
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'contractor_org_id');
|
|
2178
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
2179
|
-
|
|
2180
|
-
-- Build WHERE clause from filters
|
|
2181
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
2182
|
-
LOOP
|
|
2183
|
-
-- Handle simple value (exact match)
|
|
2184
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
2185
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
2186
|
-
ELSE
|
|
2187
|
-
-- Handle operator-based filters
|
|
2188
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
2189
|
-
LOOP
|
|
2190
|
-
CASE v_operator
|
|
2191
|
-
WHEN 'eq' THEN
|
|
2192
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
2193
|
-
WHEN 'ne' THEN
|
|
2194
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
2195
|
-
WHEN 'gt' THEN
|
|
2196
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
2197
|
-
WHEN 'gte' THEN
|
|
2198
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
2199
|
-
WHEN 'lt' THEN
|
|
2200
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
2201
|
-
WHEN 'lte' THEN
|
|
2202
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
2203
|
-
WHEN 'in' THEN
|
|
2204
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
2205
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2206
|
-
WHEN 'not_in' THEN
|
|
2207
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
2208
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2209
|
-
WHEN 'like' THEN
|
|
2210
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
2211
|
-
WHEN 'ilike' THEN
|
|
2212
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
2213
|
-
WHEN 'is_null' THEN
|
|
2214
|
-
IF (v_value::text = 'true') THEN
|
|
2215
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
2216
|
-
END IF;
|
|
2217
|
-
WHEN 'not_null' THEN
|
|
2218
|
-
IF (v_value::text = 'true') THEN
|
|
2219
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
2220
|
-
END IF;
|
|
2221
|
-
ELSE
|
|
2222
|
-
-- Unknown operator, skip
|
|
2223
|
-
END CASE;
|
|
2224
|
-
END LOOP;
|
|
2225
|
-
END IF;
|
|
2226
|
-
END LOOP;
|
|
2227
|
-
|
|
2228
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
2229
|
-
EXECUTE format('
|
|
2230
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
2231
|
-
FROM (
|
|
2232
|
-
SELECT * FROM contractor_rights
|
|
2233
|
-
WHERE (TRUE) %s
|
|
2234
|
-
ORDER BY %I %s
|
|
2235
|
-
LIMIT %L OFFSET %L
|
|
2236
|
-
) t
|
|
2237
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
2238
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
2239
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
2240
|
-
INTO v_results;
|
|
2241
|
-
|
|
2242
|
-
RETURN v_results;
|
|
2243
|
-
END;
|
|
2244
|
-
$$;
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
CREATE TABLE IF NOT EXISTS brands (
|
|
2248
|
-
id serial PRIMARY KEY,
|
|
2249
|
-
org_id int NOT NULL REFERENCES organisations(id) ON DELETE CASCADE,
|
|
2250
|
-
name text NOT NULL,
|
|
2251
|
-
description text
|
|
2252
|
-
);
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_brands(p_user_id int, p_data jsonb)
|
|
2256
|
-
RETURNS jsonb
|
|
2257
|
-
LANGUAGE plpgsql
|
|
2258
|
-
SECURITY DEFINER
|
|
2259
|
-
SET search_path = dzql_v2, public
|
|
2260
|
-
AS $$
|
|
2261
|
-
DECLARE
|
|
2262
|
-
v_result jsonb;
|
|
2263
|
-
v_old_data jsonb;
|
|
2264
|
-
v_commit_id bigint;
|
|
2265
|
-
v_op text;
|
|
2266
|
-
v_tag_ids INT[];
|
|
2267
|
-
BEGIN
|
|
2268
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2269
|
-
|
|
2270
|
-
-- M2M: Extract tags IDs
|
|
2271
|
-
IF p_data ? 'tag_ids' THEN
|
|
2272
|
-
v_tag_ids := ARRAY(SELECT jsonb_array_elements_text(p_data->'tag_ids')::int);
|
|
2273
|
-
p_data := p_data - 'tag_ids';
|
|
2274
|
-
END IF;
|
|
2275
|
-
|
|
2276
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
2277
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM brands WHERE id = (p_data->>'id')::int) THEN
|
|
2278
|
-
v_op := 'update';
|
|
2279
|
-
|
|
2280
|
-
-- Fetch old data for update rules/events
|
|
2281
|
-
SELECT to_jsonb(brands.*) INTO v_old_data FROM brands WHERE id = (p_data->>'id')::int;
|
|
2282
|
-
|
|
2283
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2284
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2285
|
-
END IF;
|
|
2286
|
-
|
|
2287
|
-
-- Perform Partial Update
|
|
2288
|
-
UPDATE brands SET
|
|
2289
|
-
org_id = CASE WHEN (p_data ? 'org_id') THEN (p_data->>'org_id')::int ELSE org_id END,
|
|
2290
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
2291
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END
|
|
2292
|
-
WHERE id = (p_data->>'id')::int
|
|
2293
|
-
RETURNING to_jsonb(brands.*) INTO v_result;
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
ELSE
|
|
2298
|
-
v_op := 'insert';
|
|
2299
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (p_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2300
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2301
|
-
END IF;
|
|
2302
|
-
|
|
2303
|
-
-- Perform Insert
|
|
2304
|
-
INSERT INTO brands (org_id, name, description)
|
|
2305
|
-
VALUES ((p_data->>'org_id')::int, (p_data->>'name'), (p_data->>'description'))
|
|
2306
|
-
RETURNING to_jsonb(brands.*) INTO v_result;
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
END IF;
|
|
2310
|
-
|
|
2311
|
-
-- M2M Sync: tags (junction: brand_tags)
|
|
2312
|
-
IF v_tag_ids IS NOT NULL THEN
|
|
2313
|
-
-- Delete relationships not in new list
|
|
2314
|
-
DELETE FROM brand_tags
|
|
2315
|
-
WHERE brand_id = (v_result->>'id')::int
|
|
2316
|
-
AND (tag_id <> ALL(v_tag_ids) OR v_tag_ids = '{}');
|
|
2317
|
-
|
|
2318
|
-
-- Insert new relationships (idempotent)
|
|
2319
|
-
IF array_length(v_tag_ids, 1) > 0 THEN
|
|
2320
|
-
INSERT INTO brand_tags (brand_id, tag_id)
|
|
2321
|
-
SELECT (v_result->>'id')::int, unnest(v_tag_ids)
|
|
2322
|
-
ON CONFLICT (brand_id, tag_id) DO NOTHING;
|
|
2323
|
-
END IF;
|
|
2324
|
-
END IF;
|
|
2325
|
-
|
|
2326
|
-
-- M2M: Add tag_ids to output
|
|
2327
|
-
v_result := v_result || jsonb_build_object('tag_ids',
|
|
2328
|
-
(SELECT COALESCE(jsonb_agg(tag_id ORDER BY tag_id), '[]'::jsonb)
|
|
2329
|
-
FROM brand_tags WHERE brand_id = (v_result->>'id')::int));
|
|
2330
|
-
|
|
2331
|
-
-- Emit Event
|
|
2332
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2333
|
-
VALUES (
|
|
2334
|
-
v_commit_id,
|
|
2335
|
-
'brands',
|
|
2336
|
-
v_op,
|
|
2337
|
-
jsonb_build_object('id', v_result->'id'),
|
|
2338
|
-
v_result,
|
|
2339
|
-
v_old_data, -- NULL for insert
|
|
2340
|
-
p_user_id
|
|
2341
|
-
);
|
|
2342
|
-
|
|
2343
|
-
-- Notify Runtime
|
|
2344
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2345
|
-
|
|
2346
|
-
-- Remove hidden fields before returning to client
|
|
2347
|
-
RETURN v_result;
|
|
2348
|
-
END;
|
|
2349
|
-
$$;
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_brands(p_user_id int, p_pk jsonb)
|
|
2353
|
-
RETURNS jsonb
|
|
2354
|
-
LANGUAGE plpgsql
|
|
2355
|
-
SECURITY DEFINER
|
|
2356
|
-
SET search_path = dzql_v2, public
|
|
2357
|
-
AS $$
|
|
2358
|
-
DECLARE
|
|
2359
|
-
v_old_data jsonb;
|
|
2360
|
-
v_commit_id bigint;
|
|
2361
|
-
BEGIN
|
|
2362
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2363
|
-
|
|
2364
|
-
-- Fetch old data FIRST for permission check
|
|
2365
|
-
SELECT to_jsonb(brands.*) INTO v_old_data FROM brands WHERE id = (p_pk->>'id')::int;
|
|
2366
|
-
|
|
2367
|
-
IF v_old_data IS NULL THEN
|
|
2368
|
-
RAISE EXCEPTION 'not_found';
|
|
2369
|
-
END IF;
|
|
2370
|
-
|
|
2371
|
-
-- Permission Check (Delete)
|
|
2372
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (v_old_data->>'org_id')::int AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2373
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2374
|
-
END IF;
|
|
2375
|
-
|
|
2376
|
-
-- Graph Rules (Pre-delete cascades)
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
-- Perform Delete
|
|
2380
|
-
DELETE FROM brands WHERE id = (p_pk->>'id')::int;
|
|
2381
|
-
|
|
2382
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
2383
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2384
|
-
VALUES (
|
|
2385
|
-
v_commit_id,
|
|
2386
|
-
'brands',
|
|
2387
|
-
'delete',
|
|
2388
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
2389
|
-
v_old_data, -- Include full data for subscription resolution
|
|
2390
|
-
v_old_data,
|
|
2391
|
-
p_user_id
|
|
2392
|
-
);
|
|
2393
|
-
|
|
2394
|
-
-- Notify Runtime
|
|
2395
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2396
|
-
|
|
2397
|
-
-- Remove hidden fields before returning to client
|
|
2398
|
-
RETURN v_old_data;
|
|
2399
|
-
END;
|
|
2400
|
-
$$;
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_brands(p_user_id int, p_pk jsonb)
|
|
2404
|
-
RETURNS jsonb
|
|
2405
|
-
LANGUAGE plpgsql
|
|
2406
|
-
SECURITY DEFINER
|
|
2407
|
-
SET search_path = dzql_v2, public
|
|
2408
|
-
AS $$
|
|
2409
|
-
DECLARE
|
|
2410
|
-
v_result jsonb;
|
|
2411
|
-
BEGIN
|
|
2412
|
-
SELECT to_jsonb(brands.*) INTO v_result
|
|
2413
|
-
FROM brands
|
|
2414
|
-
WHERE id = (p_pk->>'id')::int
|
|
2415
|
-
AND (TRUE);
|
|
2416
|
-
|
|
2417
|
-
IF v_result IS NULL THEN
|
|
2418
|
-
RETURN NULL;
|
|
2419
|
-
END IF;
|
|
2420
|
-
|
|
2421
|
-
-- M2M: Add tag_ids to result
|
|
2422
|
-
v_result := v_result || jsonb_build_object('tag_ids',
|
|
2423
|
-
(SELECT COALESCE(jsonb_agg(tag_id ORDER BY tag_id), '[]'::jsonb)
|
|
2424
|
-
FROM brand_tags WHERE brand_id = (v_result->>'id')::int));
|
|
2425
|
-
|
|
2426
|
-
RETURN v_result;
|
|
2427
|
-
END;
|
|
2428
|
-
$$;
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_brands(p_user_id int, p_query jsonb)
|
|
2432
|
-
RETURNS jsonb
|
|
2433
|
-
LANGUAGE plpgsql
|
|
2434
|
-
SECURITY DEFINER
|
|
2435
|
-
SET search_path = dzql_v2, public
|
|
2436
|
-
AS $$
|
|
2437
|
-
DECLARE
|
|
2438
|
-
v_results jsonb;
|
|
2439
|
-
v_filters jsonb;
|
|
2440
|
-
v_sort_field text;
|
|
2441
|
-
v_sort_order text;
|
|
2442
|
-
v_where_clause text := '';
|
|
2443
|
-
v_field text;
|
|
2444
|
-
v_filter jsonb;
|
|
2445
|
-
v_operator text;
|
|
2446
|
-
v_value jsonb;
|
|
2447
|
-
BEGIN
|
|
2448
|
-
-- Extract query parameters
|
|
2449
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
2450
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
2451
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
2452
|
-
|
|
2453
|
-
-- Build WHERE clause from filters
|
|
2454
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
2455
|
-
LOOP
|
|
2456
|
-
-- Handle simple value (exact match)
|
|
2457
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
2458
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
2459
|
-
ELSE
|
|
2460
|
-
-- Handle operator-based filters
|
|
2461
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
2462
|
-
LOOP
|
|
2463
|
-
CASE v_operator
|
|
2464
|
-
WHEN 'eq' THEN
|
|
2465
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
2466
|
-
WHEN 'ne' THEN
|
|
2467
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
2468
|
-
WHEN 'gt' THEN
|
|
2469
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
2470
|
-
WHEN 'gte' THEN
|
|
2471
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
2472
|
-
WHEN 'lt' THEN
|
|
2473
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
2474
|
-
WHEN 'lte' THEN
|
|
2475
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
2476
|
-
WHEN 'in' THEN
|
|
2477
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
2478
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2479
|
-
WHEN 'not_in' THEN
|
|
2480
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
2481
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2482
|
-
WHEN 'like' THEN
|
|
2483
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
2484
|
-
WHEN 'ilike' THEN
|
|
2485
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
2486
|
-
WHEN 'is_null' THEN
|
|
2487
|
-
IF (v_value::text = 'true') THEN
|
|
2488
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
2489
|
-
END IF;
|
|
2490
|
-
WHEN 'not_null' THEN
|
|
2491
|
-
IF (v_value::text = 'true') THEN
|
|
2492
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
2493
|
-
END IF;
|
|
2494
|
-
ELSE
|
|
2495
|
-
-- Unknown operator, skip
|
|
2496
|
-
END CASE;
|
|
2497
|
-
END LOOP;
|
|
2498
|
-
END IF;
|
|
2499
|
-
END LOOP;
|
|
2500
|
-
|
|
2501
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
2502
|
-
EXECUTE format('
|
|
2503
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*) || jsonb_build_object(''tag_ids'', m2m_tag_ids.tag_ids)), ''[]''::jsonb)
|
|
2504
|
-
FROM (
|
|
2505
|
-
SELECT * FROM brands
|
|
2506
|
-
WHERE (TRUE) %s
|
|
2507
|
-
ORDER BY %I %s
|
|
2508
|
-
LIMIT %L OFFSET %L
|
|
2509
|
-
) t
|
|
2510
|
-
LEFT JOIN LATERAL (
|
|
2511
|
-
SELECT COALESCE(jsonb_agg(tag_id ORDER BY tag_id), ''[]''::jsonb) as tag_ids
|
|
2512
|
-
FROM brand_tags
|
|
2513
|
-
WHERE brand_id = t.id
|
|
2514
|
-
) m2m_tag_ids ON true
|
|
2515
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
2516
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
2517
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
2518
|
-
INTO v_results;
|
|
2519
|
-
|
|
2520
|
-
RETURN v_results;
|
|
2521
|
-
END;
|
|
2522
|
-
$$;
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
CREATE TABLE IF NOT EXISTS artwork (
|
|
2526
|
-
id serial PRIMARY KEY,
|
|
2527
|
-
brand_id int NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
|
|
2528
|
-
url text NOT NULL,
|
|
2529
|
-
ratio decimal(10, 4) NOT NULL
|
|
2530
|
-
);
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_artwork(p_user_id int, p_data jsonb)
|
|
2534
|
-
RETURNS jsonb
|
|
2535
|
-
LANGUAGE plpgsql
|
|
2536
|
-
SECURITY DEFINER
|
|
2537
|
-
SET search_path = dzql_v2, public
|
|
2538
|
-
AS $$
|
|
2539
|
-
DECLARE
|
|
2540
|
-
v_result jsonb;
|
|
2541
|
-
v_old_data jsonb;
|
|
2542
|
-
v_commit_id bigint;
|
|
2543
|
-
v_op text;
|
|
2544
|
-
|
|
2545
|
-
BEGIN
|
|
2546
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
2550
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM artwork WHERE id = (p_data->>'id')::int) THEN
|
|
2551
|
-
v_op := 'update';
|
|
2552
|
-
|
|
2553
|
-
-- Fetch old data for update rules/events
|
|
2554
|
-
SELECT to_jsonb(artwork.*) INTO v_old_data FROM artwork WHERE id = (p_data->>'id')::int;
|
|
2555
|
-
|
|
2556
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM brands WHERE id = (p_data->>'brand_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2557
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2558
|
-
END IF;
|
|
2559
|
-
|
|
2560
|
-
-- Perform Partial Update
|
|
2561
|
-
UPDATE artwork SET
|
|
2562
|
-
brand_id = CASE WHEN (p_data ? 'brand_id') THEN (p_data->>'brand_id')::int ELSE brand_id END,
|
|
2563
|
-
url = CASE WHEN (p_data ? 'url') THEN (p_data->>'url') ELSE url END,
|
|
2564
|
-
ratio = CASE WHEN (p_data ? 'ratio') THEN (p_data->>'ratio')::numeric ELSE ratio END
|
|
2565
|
-
WHERE id = (p_data->>'id')::int
|
|
2566
|
-
RETURNING to_jsonb(artwork.*) INTO v_result;
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
ELSE
|
|
2571
|
-
v_op := 'insert';
|
|
2572
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM brands WHERE id = (p_data->>'brand_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2573
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2574
|
-
END IF;
|
|
2575
|
-
|
|
2576
|
-
-- Perform Insert
|
|
2577
|
-
INSERT INTO artwork (brand_id, url, ratio)
|
|
2578
|
-
VALUES ((p_data->>'brand_id')::int, (p_data->>'url'), (p_data->>'ratio')::numeric)
|
|
2579
|
-
RETURNING to_jsonb(artwork.*) INTO v_result;
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
END IF;
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
-- Emit Event
|
|
2587
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2588
|
-
VALUES (
|
|
2589
|
-
v_commit_id,
|
|
2590
|
-
'artwork',
|
|
2591
|
-
v_op,
|
|
2592
|
-
jsonb_build_object('id', v_result->'id'),
|
|
2593
|
-
v_result,
|
|
2594
|
-
v_old_data, -- NULL for insert
|
|
2595
|
-
p_user_id
|
|
2596
|
-
);
|
|
2597
|
-
|
|
2598
|
-
-- Notify Runtime
|
|
2599
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2600
|
-
|
|
2601
|
-
-- Remove hidden fields before returning to client
|
|
2602
|
-
RETURN v_result;
|
|
2603
|
-
END;
|
|
2604
|
-
$$;
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_artwork(p_user_id int, p_pk jsonb)
|
|
2608
|
-
RETURNS jsonb
|
|
2609
|
-
LANGUAGE plpgsql
|
|
2610
|
-
SECURITY DEFINER
|
|
2611
|
-
SET search_path = dzql_v2, public
|
|
2612
|
-
AS $$
|
|
2613
|
-
DECLARE
|
|
2614
|
-
v_old_data jsonb;
|
|
2615
|
-
v_commit_id bigint;
|
|
2616
|
-
BEGIN
|
|
2617
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2618
|
-
|
|
2619
|
-
-- Fetch old data FIRST for permission check
|
|
2620
|
-
SELECT to_jsonb(artwork.*) INTO v_old_data FROM artwork WHERE id = (p_pk->>'id')::int;
|
|
2621
|
-
|
|
2622
|
-
IF v_old_data IS NULL THEN
|
|
2623
|
-
RAISE EXCEPTION 'not_found';
|
|
2624
|
-
END IF;
|
|
2625
|
-
|
|
2626
|
-
-- Permission Check (Delete)
|
|
2627
|
-
IF NOT (EXISTS (SELECT 1 FROM acts_for WHERE acts_for.org_id = (SELECT org_id FROM brands WHERE id = (v_old_data->>'brand_id')::int) AND acts_for.active = true AND acts_for.user_id = p_user_id)) THEN
|
|
2628
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2629
|
-
END IF;
|
|
2630
|
-
|
|
2631
|
-
-- Graph Rules (Pre-delete cascades)
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
-- Perform Delete
|
|
2635
|
-
DELETE FROM artwork WHERE id = (p_pk->>'id')::int;
|
|
2636
|
-
|
|
2637
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
2638
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2639
|
-
VALUES (
|
|
2640
|
-
v_commit_id,
|
|
2641
|
-
'artwork',
|
|
2642
|
-
'delete',
|
|
2643
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
2644
|
-
v_old_data, -- Include full data for subscription resolution
|
|
2645
|
-
v_old_data,
|
|
2646
|
-
p_user_id
|
|
2647
|
-
);
|
|
2648
|
-
|
|
2649
|
-
-- Notify Runtime
|
|
2650
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2651
|
-
|
|
2652
|
-
-- Remove hidden fields before returning to client
|
|
2653
|
-
RETURN v_old_data;
|
|
2654
|
-
END;
|
|
2655
|
-
$$;
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_artwork(p_user_id int, p_pk jsonb)
|
|
2659
|
-
RETURNS jsonb
|
|
2660
|
-
LANGUAGE plpgsql
|
|
2661
|
-
SECURITY DEFINER
|
|
2662
|
-
SET search_path = dzql_v2, public
|
|
2663
|
-
AS $$
|
|
2664
|
-
DECLARE
|
|
2665
|
-
v_result jsonb;
|
|
2666
|
-
BEGIN
|
|
2667
|
-
SELECT to_jsonb(artwork.*) INTO v_result
|
|
2668
|
-
FROM artwork
|
|
2669
|
-
WHERE id = (p_pk->>'id')::int
|
|
2670
|
-
AND (TRUE);
|
|
2671
|
-
|
|
2672
|
-
IF v_result IS NULL THEN
|
|
2673
|
-
RETURN NULL;
|
|
2674
|
-
END IF;
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
RETURN v_result;
|
|
2678
|
-
END;
|
|
2679
|
-
$$;
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_artwork(p_user_id int, p_query jsonb)
|
|
2683
|
-
RETURNS jsonb
|
|
2684
|
-
LANGUAGE plpgsql
|
|
2685
|
-
SECURITY DEFINER
|
|
2686
|
-
SET search_path = dzql_v2, public
|
|
2687
|
-
AS $$
|
|
2688
|
-
DECLARE
|
|
2689
|
-
v_results jsonb;
|
|
2690
|
-
v_filters jsonb;
|
|
2691
|
-
v_sort_field text;
|
|
2692
|
-
v_sort_order text;
|
|
2693
|
-
v_where_clause text := '';
|
|
2694
|
-
v_field text;
|
|
2695
|
-
v_filter jsonb;
|
|
2696
|
-
v_operator text;
|
|
2697
|
-
v_value jsonb;
|
|
2698
|
-
BEGIN
|
|
2699
|
-
-- Extract query parameters
|
|
2700
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
2701
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'url');
|
|
2702
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
2703
|
-
|
|
2704
|
-
-- Build WHERE clause from filters
|
|
2705
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
2706
|
-
LOOP
|
|
2707
|
-
-- Handle simple value (exact match)
|
|
2708
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
2709
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
2710
|
-
ELSE
|
|
2711
|
-
-- Handle operator-based filters
|
|
2712
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
2713
|
-
LOOP
|
|
2714
|
-
CASE v_operator
|
|
2715
|
-
WHEN 'eq' THEN
|
|
2716
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
2717
|
-
WHEN 'ne' THEN
|
|
2718
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
2719
|
-
WHEN 'gt' THEN
|
|
2720
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
2721
|
-
WHEN 'gte' THEN
|
|
2722
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
2723
|
-
WHEN 'lt' THEN
|
|
2724
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
2725
|
-
WHEN 'lte' THEN
|
|
2726
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
2727
|
-
WHEN 'in' THEN
|
|
2728
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
2729
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2730
|
-
WHEN 'not_in' THEN
|
|
2731
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
2732
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2733
|
-
WHEN 'like' THEN
|
|
2734
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
2735
|
-
WHEN 'ilike' THEN
|
|
2736
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
2737
|
-
WHEN 'is_null' THEN
|
|
2738
|
-
IF (v_value::text = 'true') THEN
|
|
2739
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
2740
|
-
END IF;
|
|
2741
|
-
WHEN 'not_null' THEN
|
|
2742
|
-
IF (v_value::text = 'true') THEN
|
|
2743
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
2744
|
-
END IF;
|
|
2745
|
-
ELSE
|
|
2746
|
-
-- Unknown operator, skip
|
|
2747
|
-
END CASE;
|
|
2748
|
-
END LOOP;
|
|
2749
|
-
END IF;
|
|
2750
|
-
END LOOP;
|
|
2751
|
-
|
|
2752
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
2753
|
-
EXECUTE format('
|
|
2754
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
2755
|
-
FROM (
|
|
2756
|
-
SELECT * FROM artwork
|
|
2757
|
-
WHERE (TRUE) %s
|
|
2758
|
-
ORDER BY %I %s
|
|
2759
|
-
LIMIT %L OFFSET %L
|
|
2760
|
-
) t
|
|
2761
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
2762
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
2763
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
2764
|
-
INTO v_results;
|
|
2765
|
-
|
|
2766
|
-
RETURN v_results;
|
|
2767
|
-
END;
|
|
2768
|
-
$$;
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
CREATE TABLE IF NOT EXISTS tags (
|
|
2772
|
-
id serial PRIMARY KEY,
|
|
2773
|
-
name text NOT NULL UNIQUE,
|
|
2774
|
-
color text,
|
|
2775
|
-
description text
|
|
2776
|
-
);
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
CREATE OR REPLACE FUNCTION dzql_v2.save_tags(p_user_id int, p_data jsonb)
|
|
2780
|
-
RETURNS jsonb
|
|
2781
|
-
LANGUAGE plpgsql
|
|
2782
|
-
SECURITY DEFINER
|
|
2783
|
-
SET search_path = dzql_v2, public
|
|
2784
|
-
AS $$
|
|
2785
|
-
DECLARE
|
|
2786
|
-
v_result jsonb;
|
|
2787
|
-
v_old_data jsonb;
|
|
2788
|
-
v_commit_id bigint;
|
|
2789
|
-
v_op text;
|
|
2790
|
-
|
|
2791
|
-
BEGIN
|
|
2792
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
-- Determine Operation & Check Permissions (supports composite PK)
|
|
2796
|
-
IF ((p_data->>'id') IS NOT NULL) AND EXISTS(SELECT 1 FROM tags WHERE id = (p_data->>'id')::int) THEN
|
|
2797
|
-
v_op := 'update';
|
|
2798
|
-
|
|
2799
|
-
-- Fetch old data for update rules/events
|
|
2800
|
-
SELECT to_jsonb(tags.*) INTO v_old_data FROM tags WHERE id = (p_data->>'id')::int;
|
|
2801
|
-
|
|
2802
|
-
IF NOT (TRUE) THEN
|
|
2803
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2804
|
-
END IF;
|
|
2805
|
-
|
|
2806
|
-
-- Perform Partial Update
|
|
2807
|
-
UPDATE tags SET
|
|
2808
|
-
name = CASE WHEN (p_data ? 'name') THEN (p_data->>'name') ELSE name END,
|
|
2809
|
-
color = CASE WHEN (p_data ? 'color') THEN (p_data->>'color') ELSE color END,
|
|
2810
|
-
description = CASE WHEN (p_data ? 'description') THEN (p_data->>'description') ELSE description END
|
|
2811
|
-
WHERE id = (p_data->>'id')::int
|
|
2812
|
-
RETURNING to_jsonb(tags.*) INTO v_result;
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
ELSE
|
|
2817
|
-
v_op := 'insert';
|
|
2818
|
-
IF NOT (TRUE) THEN
|
|
2819
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2820
|
-
END IF;
|
|
2821
|
-
|
|
2822
|
-
-- Perform Insert
|
|
2823
|
-
INSERT INTO tags (name, color, description)
|
|
2824
|
-
VALUES ((p_data->>'name'), (p_data->>'color'), (p_data->>'description'))
|
|
2825
|
-
RETURNING to_jsonb(tags.*) INTO v_result;
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
END IF;
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
-- Emit Event
|
|
2833
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2834
|
-
VALUES (
|
|
2835
|
-
v_commit_id,
|
|
2836
|
-
'tags',
|
|
2837
|
-
v_op,
|
|
2838
|
-
jsonb_build_object('id', v_result->'id'),
|
|
2839
|
-
v_result,
|
|
2840
|
-
v_old_data, -- NULL for insert
|
|
2841
|
-
p_user_id
|
|
2842
|
-
);
|
|
2843
|
-
|
|
2844
|
-
-- Notify Runtime
|
|
2845
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2846
|
-
|
|
2847
|
-
-- Remove hidden fields before returning to client
|
|
2848
|
-
RETURN v_result;
|
|
2849
|
-
END;
|
|
2850
|
-
$$;
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
CREATE OR REPLACE FUNCTION dzql_v2.delete_tags(p_user_id int, p_pk jsonb)
|
|
2854
|
-
RETURNS jsonb
|
|
2855
|
-
LANGUAGE plpgsql
|
|
2856
|
-
SECURITY DEFINER
|
|
2857
|
-
SET search_path = dzql_v2, public
|
|
2858
|
-
AS $$
|
|
2859
|
-
DECLARE
|
|
2860
|
-
v_old_data jsonb;
|
|
2861
|
-
v_commit_id bigint;
|
|
2862
|
-
BEGIN
|
|
2863
|
-
v_commit_id := nextval('dzql_v2.commit_seq');
|
|
2864
|
-
|
|
2865
|
-
-- Fetch old data FIRST for permission check
|
|
2866
|
-
SELECT to_jsonb(tags.*) INTO v_old_data FROM tags WHERE id = (p_pk->>'id')::int;
|
|
2867
|
-
|
|
2868
|
-
IF v_old_data IS NULL THEN
|
|
2869
|
-
RAISE EXCEPTION 'not_found';
|
|
2870
|
-
END IF;
|
|
2871
|
-
|
|
2872
|
-
-- Permission Check (Delete)
|
|
2873
|
-
IF NOT (TRUE) THEN
|
|
2874
|
-
RAISE EXCEPTION 'permission_denied';
|
|
2875
|
-
END IF;
|
|
2876
|
-
|
|
2877
|
-
-- Graph Rules (Pre-delete cascades)
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
-- Perform Delete
|
|
2881
|
-
DELETE FROM tags WHERE id = (p_pk->>'id')::int;
|
|
2882
|
-
|
|
2883
|
-
-- Emit Event (always 'delete' operation for client-side removal)
|
|
2884
|
-
INSERT INTO dzql_v2.events (commit_id, table_name, op, pk, data, old_data, user_id)
|
|
2885
|
-
VALUES (
|
|
2886
|
-
v_commit_id,
|
|
2887
|
-
'tags',
|
|
2888
|
-
'delete',
|
|
2889
|
-
jsonb_build_object('id', v_old_data->'id'),
|
|
2890
|
-
v_old_data, -- Include full data for subscription resolution
|
|
2891
|
-
v_old_data,
|
|
2892
|
-
p_user_id
|
|
2893
|
-
);
|
|
2894
|
-
|
|
2895
|
-
-- Notify Runtime
|
|
2896
|
-
PERFORM pg_notify('dzql_v2', json_build_object('commit_id', v_commit_id)::text);
|
|
2897
|
-
|
|
2898
|
-
-- Remove hidden fields before returning to client
|
|
2899
|
-
RETURN v_old_data;
|
|
2900
|
-
END;
|
|
2901
|
-
$$;
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
CREATE OR REPLACE FUNCTION dzql_v2.get_tags(p_user_id int, p_pk jsonb)
|
|
2905
|
-
RETURNS jsonb
|
|
2906
|
-
LANGUAGE plpgsql
|
|
2907
|
-
SECURITY DEFINER
|
|
2908
|
-
SET search_path = dzql_v2, public
|
|
2909
|
-
AS $$
|
|
2910
|
-
DECLARE
|
|
2911
|
-
v_result jsonb;
|
|
2912
|
-
BEGIN
|
|
2913
|
-
SELECT to_jsonb(tags.*) INTO v_result
|
|
2914
|
-
FROM tags
|
|
2915
|
-
WHERE id = (p_pk->>'id')::int
|
|
2916
|
-
AND (TRUE);
|
|
2917
|
-
|
|
2918
|
-
IF v_result IS NULL THEN
|
|
2919
|
-
RETURN NULL;
|
|
2920
|
-
END IF;
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
RETURN v_result;
|
|
2924
|
-
END;
|
|
2925
|
-
$$;
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
CREATE OR REPLACE FUNCTION dzql_v2.search_tags(p_user_id int, p_query jsonb)
|
|
2929
|
-
RETURNS jsonb
|
|
2930
|
-
LANGUAGE plpgsql
|
|
2931
|
-
SECURITY DEFINER
|
|
2932
|
-
SET search_path = dzql_v2, public
|
|
2933
|
-
AS $$
|
|
2934
|
-
DECLARE
|
|
2935
|
-
v_results jsonb;
|
|
2936
|
-
v_filters jsonb;
|
|
2937
|
-
v_sort_field text;
|
|
2938
|
-
v_sort_order text;
|
|
2939
|
-
v_where_clause text := '';
|
|
2940
|
-
v_field text;
|
|
2941
|
-
v_filter jsonb;
|
|
2942
|
-
v_operator text;
|
|
2943
|
-
v_value jsonb;
|
|
2944
|
-
BEGIN
|
|
2945
|
-
-- Extract query parameters
|
|
2946
|
-
v_filters := COALESCE(p_query->'filters', '{}'::jsonb);
|
|
2947
|
-
v_sort_field := COALESCE(p_query->>'sort_field', 'name');
|
|
2948
|
-
v_sort_order := COALESCE(p_query->>'sort_order', 'asc');
|
|
2949
|
-
|
|
2950
|
-
-- Build WHERE clause from filters
|
|
2951
|
-
FOR v_field, v_filter IN SELECT * FROM jsonb_each(v_filters)
|
|
2952
|
-
LOOP
|
|
2953
|
-
-- Handle simple value (exact match)
|
|
2954
|
-
IF jsonb_typeof(v_filter) IN ('string', 'number', 'boolean') THEN
|
|
2955
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_filter #>> '{}');
|
|
2956
|
-
ELSE
|
|
2957
|
-
-- Handle operator-based filters
|
|
2958
|
-
FOR v_operator, v_value IN SELECT * FROM jsonb_each(v_filter)
|
|
2959
|
-
LOOP
|
|
2960
|
-
CASE v_operator
|
|
2961
|
-
WHEN 'eq' THEN
|
|
2962
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = %L', v_field, v_value #>> '{}');
|
|
2963
|
-
WHEN 'ne' THEN
|
|
2964
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != %L', v_field, v_value #>> '{}');
|
|
2965
|
-
WHEN 'gt' THEN
|
|
2966
|
-
v_where_clause := v_where_clause || format(' AND %I > %L', v_field, v_value #>> '{}');
|
|
2967
|
-
WHEN 'gte' THEN
|
|
2968
|
-
v_where_clause := v_where_clause || format(' AND %I >= %L', v_field, v_value #>> '{}');
|
|
2969
|
-
WHEN 'lt' THEN
|
|
2970
|
-
v_where_clause := v_where_clause || format(' AND %I < %L', v_field, v_value #>> '{}');
|
|
2971
|
-
WHEN 'lte' THEN
|
|
2972
|
-
v_where_clause := v_where_clause || format(' AND %I <= %L', v_field, v_value #>> '{}');
|
|
2973
|
-
WHEN 'in' THEN
|
|
2974
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT = ANY(%L)', v_field,
|
|
2975
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2976
|
-
WHEN 'not_in' THEN
|
|
2977
|
-
v_where_clause := v_where_clause || format(' AND %I::TEXT != ALL(%L)', v_field,
|
|
2978
|
-
(SELECT array_agg(value #>> '{}') FROM jsonb_array_elements(v_value) AS value));
|
|
2979
|
-
WHEN 'like' THEN
|
|
2980
|
-
v_where_clause := v_where_clause || format(' AND %I LIKE %L', v_field, v_value #>> '{}');
|
|
2981
|
-
WHEN 'ilike' THEN
|
|
2982
|
-
v_where_clause := v_where_clause || format(' AND %I ILIKE %L', v_field, v_value #>> '{}');
|
|
2983
|
-
WHEN 'is_null' THEN
|
|
2984
|
-
IF (v_value::text = 'true') THEN
|
|
2985
|
-
v_where_clause := v_where_clause || format(' AND %I IS NULL', v_field);
|
|
2986
|
-
END IF;
|
|
2987
|
-
WHEN 'not_null' THEN
|
|
2988
|
-
IF (v_value::text = 'true') THEN
|
|
2989
|
-
v_where_clause := v_where_clause || format(' AND %I IS NOT NULL', v_field);
|
|
2990
|
-
END IF;
|
|
2991
|
-
ELSE
|
|
2992
|
-
-- Unknown operator, skip
|
|
2993
|
-
END CASE;
|
|
2994
|
-
END LOOP;
|
|
2995
|
-
END IF;
|
|
2996
|
-
END LOOP;
|
|
2997
|
-
|
|
2998
|
-
-- Execute dynamic query (sort inside subquery for correct LIMIT behavior)
|
|
2999
|
-
EXECUTE format('
|
|
3000
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
3001
|
-
FROM (
|
|
3002
|
-
SELECT * FROM tags
|
|
3003
|
-
WHERE (TRUE) %s
|
|
3004
|
-
ORDER BY %I %s
|
|
3005
|
-
LIMIT %L OFFSET %L
|
|
3006
|
-
) t
|
|
3007
|
-
', v_where_clause, v_sort_field, v_sort_order,
|
|
3008
|
-
COALESCE((p_query->>'limit')::int, 10),
|
|
3009
|
-
COALESCE((p_query->>'offset')::int, 0))
|
|
3010
|
-
INTO v_results;
|
|
3011
|
-
|
|
3012
|
-
RETURN v_results;
|
|
3013
|
-
END;
|
|
3014
|
-
$$;
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
CREATE TABLE IF NOT EXISTS brand_tags (
|
|
3018
|
-
brand_id int NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
|
|
3019
|
-
tag_id int NOT NULL REFERENCES tags(id) ON DELETE CASCADE
|
|
3020
|
-
);
|