dzql 0.5.33 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,581 +0,0 @@
|
|
|
1
|
-
-- DZQL Search Operations - Version 3.0.0
|
|
2
|
-
-- Advanced search and filtering capabilities for DZQL entities
|
|
3
|
-
|
|
4
|
-
-- ============================================================================
|
|
5
|
-
-- FILTER PROCESSING HELPERS
|
|
6
|
-
-- ============================================================================
|
|
7
|
-
|
|
8
|
-
-- Get column data type for proper casting
|
|
9
|
-
CREATE OR REPLACE FUNCTION dzql.get_column_type(
|
|
10
|
-
p_table_name text,
|
|
11
|
-
p_column_name text
|
|
12
|
-
) RETURNS text
|
|
13
|
-
LANGUAGE sql STABLE AS $$
|
|
14
|
-
SELECT format_type(atttypid, atttypmod)
|
|
15
|
-
FROM pg_attribute
|
|
16
|
-
WHERE attrelid = p_table_name::regclass
|
|
17
|
-
AND attname = p_column_name
|
|
18
|
-
AND NOT attisdropped
|
|
19
|
-
AND attnum > 0;
|
|
20
|
-
$$;
|
|
21
|
-
|
|
22
|
-
-- Build operator-based WHERE clause fragment using direct SQL
|
|
23
|
-
CREATE OR REPLACE FUNCTION dzql.build_operator_clause(
|
|
24
|
-
p_column_name text,
|
|
25
|
-
p_operator_obj jsonb,
|
|
26
|
-
p_column_type text
|
|
27
|
-
) RETURNS text
|
|
28
|
-
LANGUAGE plpgsql AS $$
|
|
29
|
-
DECLARE
|
|
30
|
-
l_op_key text;
|
|
31
|
-
l_op_value jsonb;
|
|
32
|
-
l_clauses text[] := array[]::text[];
|
|
33
|
-
BEGIN
|
|
34
|
-
-- Process each operator in the object
|
|
35
|
-
FOR l_op_key, l_op_value IN SELECT * FROM jsonb_each(p_operator_obj)
|
|
36
|
-
LOOP
|
|
37
|
-
CASE lower(l_op_key)
|
|
38
|
-
WHEN 'eq', '=' THEN
|
|
39
|
-
l_clauses := l_clauses || format('%I = %L', p_column_name, l_op_value#>>'{}');
|
|
40
|
-
|
|
41
|
-
WHEN 'neq', '!=', '<>' THEN
|
|
42
|
-
l_clauses := l_clauses || format('%I != %L', p_column_name, l_op_value#>>'{}');
|
|
43
|
-
|
|
44
|
-
WHEN 'gt', '>' THEN
|
|
45
|
-
l_clauses := l_clauses || format('%I > %L', p_column_name, l_op_value#>>'{}');
|
|
46
|
-
|
|
47
|
-
WHEN 'gte', '>=' THEN
|
|
48
|
-
l_clauses := l_clauses || format('%I >= %L', p_column_name, l_op_value#>>'{}');
|
|
49
|
-
|
|
50
|
-
WHEN 'lt', '<' THEN
|
|
51
|
-
l_clauses := l_clauses || format('%I < %L', p_column_name, l_op_value#>>'{}');
|
|
52
|
-
|
|
53
|
-
WHEN 'lte', '<=' THEN
|
|
54
|
-
l_clauses := l_clauses || format('%I <= %L', p_column_name, l_op_value#>>'{}');
|
|
55
|
-
|
|
56
|
-
WHEN 'like' THEN
|
|
57
|
-
l_clauses := l_clauses || format('%I LIKE %L', p_column_name, l_op_value#>>'{}');
|
|
58
|
-
|
|
59
|
-
WHEN 'ilike' THEN
|
|
60
|
-
l_clauses := l_clauses || format('%I ILIKE %L', p_column_name, l_op_value#>>'{}');
|
|
61
|
-
|
|
62
|
-
WHEN 'in' THEN
|
|
63
|
-
IF jsonb_typeof(l_op_value) = 'array' THEN
|
|
64
|
-
l_clauses := l_clauses || format('%I::TEXT = ANY(%L)',
|
|
65
|
-
p_column_name,
|
|
66
|
-
ARRAY(SELECT jsonb_array_elements_text(l_op_value))
|
|
67
|
-
);
|
|
68
|
-
END IF;
|
|
69
|
-
|
|
70
|
-
WHEN 'not_in' THEN
|
|
71
|
-
IF jsonb_typeof(l_op_value) = 'array' THEN
|
|
72
|
-
l_clauses := l_clauses || format('%I::TEXT != ALL(%L)',
|
|
73
|
-
p_column_name,
|
|
74
|
-
ARRAY(SELECT jsonb_array_elements_text(l_op_value))
|
|
75
|
-
);
|
|
76
|
-
END IF;
|
|
77
|
-
|
|
78
|
-
WHEN 'between' THEN
|
|
79
|
-
IF jsonb_typeof(l_op_value) = 'array' AND jsonb_array_length(l_op_value) = 2 THEN
|
|
80
|
-
l_clauses := l_clauses || format('%I BETWEEN %L AND %L',
|
|
81
|
-
p_column_name, l_op_value->0#>>'{}', l_op_value->1#>>'{}');
|
|
82
|
-
END IF;
|
|
83
|
-
|
|
84
|
-
WHEN 'is_null', 'null' THEN
|
|
85
|
-
IF (l_op_value::text = 'true') THEN
|
|
86
|
-
l_clauses := l_clauses || format('%I IS NULL', p_column_name);
|
|
87
|
-
END IF;
|
|
88
|
-
|
|
89
|
-
WHEN 'not_null', 'not' THEN
|
|
90
|
-
IF (l_op_value::text = 'true' OR l_op_value::text = 'null') THEN
|
|
91
|
-
l_clauses := l_clauses || format('%I IS NOT NULL', p_column_name);
|
|
92
|
-
END IF;
|
|
93
|
-
|
|
94
|
-
ELSE
|
|
95
|
-
-- Unknown operator, skip silently
|
|
96
|
-
NULL;
|
|
97
|
-
END CASE;
|
|
98
|
-
END LOOP;
|
|
99
|
-
|
|
100
|
-
IF array_length(l_clauses, 1) > 0 THEN
|
|
101
|
-
RETURN '(' || array_to_string(l_clauses, ' AND ') || ')';
|
|
102
|
-
ELSE
|
|
103
|
-
RETURN NULL;
|
|
104
|
-
END IF;
|
|
105
|
-
END $$;
|
|
106
|
-
|
|
107
|
-
-- Build complete WHERE clause from filter object
|
|
108
|
-
CREATE OR REPLACE FUNCTION dzql.build_where_clause(
|
|
109
|
-
p_table_name text,
|
|
110
|
-
p_filters jsonb
|
|
111
|
-
) RETURNS text
|
|
112
|
-
LANGUAGE plpgsql AS $$
|
|
113
|
-
DECLARE
|
|
114
|
-
l_clauses text[] := array[]::text[];
|
|
115
|
-
l_key text;
|
|
116
|
-
l_value jsonb;
|
|
117
|
-
l_column_type text;
|
|
118
|
-
l_column_exists boolean;
|
|
119
|
-
l_clause text;
|
|
120
|
-
BEGIN
|
|
121
|
-
-- Skip _search key (handled separately)
|
|
122
|
-
FOR l_key, l_value IN SELECT key, value FROM jsonb_each(p_filters)
|
|
123
|
-
LOOP
|
|
124
|
-
IF l_key = '_search' THEN
|
|
125
|
-
CONTINUE;
|
|
126
|
-
END IF;
|
|
127
|
-
|
|
128
|
-
-- Check if column exists
|
|
129
|
-
SELECT EXISTS (
|
|
130
|
-
SELECT 1 FROM pg_attribute
|
|
131
|
-
WHERE attrelid = p_table_name::regclass
|
|
132
|
-
AND attname = l_key
|
|
133
|
-
AND NOT attisdropped
|
|
134
|
-
AND attnum > 0
|
|
135
|
-
) INTO l_column_exists;
|
|
136
|
-
|
|
137
|
-
IF NOT l_column_exists THEN
|
|
138
|
-
RAISE EXCEPTION 'Column % does not exist in table %', l_key, p_table_name;
|
|
139
|
-
END IF;
|
|
140
|
-
|
|
141
|
-
-- Get column type
|
|
142
|
-
l_column_type := dzql.get_column_type(p_table_name, l_key);
|
|
143
|
-
|
|
144
|
-
-- Build clause based on value type
|
|
145
|
-
CASE jsonb_typeof(l_value)
|
|
146
|
-
WHEN 'object' THEN
|
|
147
|
-
-- Handle operator objects like {gte: 100, lt: 500}
|
|
148
|
-
l_clause := dzql.build_operator_clause(l_key, l_value, l_column_type);
|
|
149
|
-
IF l_clause IS NOT NULL THEN
|
|
150
|
-
l_clauses := l_clauses || l_clause;
|
|
151
|
-
END IF;
|
|
152
|
-
|
|
153
|
-
WHEN 'array' THEN
|
|
154
|
-
-- Handle IN clause
|
|
155
|
-
l_clauses := l_clauses || format('%I = ANY(%L)',
|
|
156
|
-
l_key,
|
|
157
|
-
ARRAY(SELECT jsonb_array_elements_text(l_value))
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
WHEN 'null' THEN
|
|
161
|
-
-- Handle IS NULL
|
|
162
|
-
l_clauses := l_clauses || format('%I IS NULL', l_key);
|
|
163
|
-
|
|
164
|
-
ELSE
|
|
165
|
-
-- Handle exact match
|
|
166
|
-
l_clauses := l_clauses || format('%I = %L', l_key, l_value#>>'{}');
|
|
167
|
-
END CASE;
|
|
168
|
-
END LOOP;
|
|
169
|
-
|
|
170
|
-
IF array_length(l_clauses, 1) > 0 THEN
|
|
171
|
-
RETURN array_to_string(l_clauses, ' AND ');
|
|
172
|
-
ELSE
|
|
173
|
-
RETURN NULL;
|
|
174
|
-
END IF;
|
|
175
|
-
END $$;
|
|
176
|
-
|
|
177
|
-
-- Build text search clause
|
|
178
|
-
CREATE OR REPLACE FUNCTION dzql.build_search_clause(
|
|
179
|
-
p_search_text text,
|
|
180
|
-
p_searchable_fields text[]
|
|
181
|
-
) RETURNS text
|
|
182
|
-
LANGUAGE plpgsql AS $$
|
|
183
|
-
DECLARE
|
|
184
|
-
l_search_clauses text[] := array[]::text[];
|
|
185
|
-
l_field text;
|
|
186
|
-
BEGIN
|
|
187
|
-
IF p_search_text IS NOT NULL AND p_search_text != '' THEN
|
|
188
|
-
FOREACH l_field IN ARRAY p_searchable_fields
|
|
189
|
-
LOOP
|
|
190
|
-
l_search_clauses := l_search_clauses ||
|
|
191
|
-
format('%I::text ILIKE %L', l_field, '%' || p_search_text || '%');
|
|
192
|
-
END LOOP;
|
|
193
|
-
|
|
194
|
-
IF array_length(l_search_clauses, 1) > 0 THEN
|
|
195
|
-
RETURN '(' || array_to_string(l_search_clauses, ' OR ') || ')';
|
|
196
|
-
END IF;
|
|
197
|
-
END IF;
|
|
198
|
-
|
|
199
|
-
RETURN NULL;
|
|
200
|
-
END $$;
|
|
201
|
-
|
|
202
|
-
-- ============================================================================
|
|
203
|
-
-- GENERIC SEARCH OPERATION
|
|
204
|
-
-- ============================================================================
|
|
205
|
-
|
|
206
|
-
-- Generic SEARCH with advanced filtering support
|
|
207
|
-
CREATE OR REPLACE FUNCTION dzql.generic_search(
|
|
208
|
-
p_entity text,
|
|
209
|
-
p_args jsonb,
|
|
210
|
-
p_user_id int
|
|
211
|
-
) RETURNS jsonb
|
|
212
|
-
LANGUAGE plpgsql
|
|
213
|
-
SECURITY INVOKER
|
|
214
|
-
AS $$
|
|
215
|
-
DECLARE
|
|
216
|
-
l_entity_config record;
|
|
217
|
-
l_filters jsonb;
|
|
218
|
-
l_where_clause text := '';
|
|
219
|
-
l_filter_clause text;
|
|
220
|
-
l_search_clause text;
|
|
221
|
-
l_temporal_clause text;
|
|
222
|
-
l_order_clause text := '';
|
|
223
|
-
l_on_date timestamptz;
|
|
224
|
-
l_page int := 1;
|
|
225
|
-
l_limit int := 50;
|
|
226
|
-
l_offset int := 0;
|
|
227
|
-
l_sort jsonb;
|
|
228
|
-
l_sort_field text;
|
|
229
|
-
l_sort_order text;
|
|
230
|
-
l_base_sql text;
|
|
231
|
-
l_count_sql text;
|
|
232
|
-
l_data_sql text;
|
|
233
|
-
l_total int;
|
|
234
|
-
l_data jsonb;
|
|
235
|
-
l_column_exists boolean;
|
|
236
|
-
l_pk_cols text[];
|
|
237
|
-
l_pk_order text;
|
|
238
|
-
l_fk_includes jsonb;
|
|
239
|
-
l_key text;
|
|
240
|
-
l_value text;
|
|
241
|
-
l_fk_result jsonb;
|
|
242
|
-
l_record jsonb;
|
|
243
|
-
l_processed_data jsonb[] := '{}';
|
|
244
|
-
i int;
|
|
245
|
-
BEGIN
|
|
246
|
-
-- Get entity configuration
|
|
247
|
-
SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_entity;
|
|
248
|
-
|
|
249
|
-
IF l_entity_config IS NULL THEN
|
|
250
|
-
RAISE EXCEPTION 'DZQL: entity % not configured', p_entity;
|
|
251
|
-
END IF;
|
|
252
|
-
|
|
253
|
-
-- Get primary key columns for default ordering
|
|
254
|
-
SELECT array_agg(a.attname ORDER BY a.attnum)
|
|
255
|
-
INTO l_pk_cols
|
|
256
|
-
FROM pg_index i
|
|
257
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
258
|
-
WHERE i.indrelid = p_entity::regclass AND i.indisprimary;
|
|
259
|
-
|
|
260
|
-
IF l_pk_cols IS NOT NULL THEN
|
|
261
|
-
l_pk_order := array_to_string(array(SELECT format('t.%I', col) FROM unnest(l_pk_cols) AS col), ', ');
|
|
262
|
-
ELSE
|
|
263
|
-
l_pk_order := 't.*';
|
|
264
|
-
END IF;
|
|
265
|
-
|
|
266
|
-
-- Extract filters and parameters
|
|
267
|
-
l_filters := COALESCE(p_args->'filters', p_args->'p_filters', '{}'::jsonb);
|
|
268
|
-
l_on_date := (p_args->>'on_date')::timestamptz;
|
|
269
|
-
|
|
270
|
-
-- Extract pagination from args or filters
|
|
271
|
-
l_page := COALESCE(
|
|
272
|
-
(p_args->>'page')::int,
|
|
273
|
-
(l_filters->>'page')::int,
|
|
274
|
-
1
|
|
275
|
-
);
|
|
276
|
-
l_limit := COALESCE(
|
|
277
|
-
(p_args->>'limit')::int,
|
|
278
|
-
(l_filters->>'limit')::int,
|
|
279
|
-
50
|
|
280
|
-
);
|
|
281
|
-
l_offset := (l_page - 1) * l_limit;
|
|
282
|
-
|
|
283
|
-
-- Build WHERE clause from filters
|
|
284
|
-
l_filter_clause := dzql.build_where_clause(p_entity, l_filters);
|
|
285
|
-
|
|
286
|
-
-- Build text search clause
|
|
287
|
-
l_search_clause := dzql.build_search_clause(
|
|
288
|
-
l_filters->>'_search',
|
|
289
|
-
l_entity_config.searchable_fields
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
-- Build temporal filter
|
|
293
|
-
l_temporal_clause := dzql.apply_temporal_filter(
|
|
294
|
-
p_entity::regclass,
|
|
295
|
-
l_entity_config.temporal_fields,
|
|
296
|
-
l_on_date
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
-- Extract sort parameters
|
|
300
|
-
l_sort := COALESCE(p_args->'sort', l_filters->'sort');
|
|
301
|
-
IF l_sort IS NOT NULL THEN
|
|
302
|
-
l_sort_field := COALESCE(l_sort->>'field', l_sort->>'column');
|
|
303
|
-
l_sort_order := COALESCE(l_sort->>'order', l_sort->>'dir', 'asc');
|
|
304
|
-
|
|
305
|
-
IF l_sort_field IS NOT NULL THEN
|
|
306
|
-
-- Validate sort field exists, fall back to 'id' if invalid
|
|
307
|
-
SELECT EXISTS (
|
|
308
|
-
SELECT 1 FROM pg_attribute
|
|
309
|
-
WHERE attrelid = p_entity::regclass
|
|
310
|
-
AND attname = l_sort_field
|
|
311
|
-
AND NOT attisdropped
|
|
312
|
-
AND attnum > 0
|
|
313
|
-
) INTO l_column_exists;
|
|
314
|
-
|
|
315
|
-
IF NOT l_column_exists THEN
|
|
316
|
-
l_sort_field := 'id'; -- Fall back to default
|
|
317
|
-
END IF;
|
|
318
|
-
|
|
319
|
-
l_order_clause := format(' ORDER BY %I %s', l_sort_field,
|
|
320
|
-
CASE WHEN upper(l_sort_order) = 'DESC' THEN 'DESC' ELSE 'ASC' END);
|
|
321
|
-
END IF;
|
|
322
|
-
END IF;
|
|
323
|
-
|
|
324
|
-
-- Build complete WHERE clause
|
|
325
|
-
IF l_filter_clause IS NOT NULL THEN
|
|
326
|
-
l_where_clause := 'WHERE ' || l_filter_clause;
|
|
327
|
-
END IF;
|
|
328
|
-
|
|
329
|
-
IF l_search_clause IS NOT NULL THEN
|
|
330
|
-
IF l_where_clause = '' THEN
|
|
331
|
-
l_where_clause := 'WHERE ' || l_search_clause;
|
|
332
|
-
ELSE
|
|
333
|
-
l_where_clause := l_where_clause || ' AND (' || l_search_clause || ')';
|
|
334
|
-
END IF;
|
|
335
|
-
END IF;
|
|
336
|
-
|
|
337
|
-
IF l_temporal_clause != '' THEN
|
|
338
|
-
IF l_where_clause = '' THEN
|
|
339
|
-
l_where_clause := 'WHERE 1=1' || l_temporal_clause;
|
|
340
|
-
ELSE
|
|
341
|
-
l_where_clause := l_where_clause || l_temporal_clause;
|
|
342
|
-
END IF;
|
|
343
|
-
END IF;
|
|
344
|
-
|
|
345
|
-
-- Add permission check to WHERE clause
|
|
346
|
-
IF l_where_clause = '' OR l_where_clause = 'WHERE' THEN
|
|
347
|
-
l_where_clause := format('WHERE dzql.check_permission(%L, ''view'', %L, to_jsonb(t.*))', p_user_id, p_entity);
|
|
348
|
-
ELSE
|
|
349
|
-
l_where_clause := l_where_clause || format(' AND dzql.check_permission(%L, ''view'', %L, to_jsonb(t.*))', p_user_id, p_entity);
|
|
350
|
-
END IF;
|
|
351
|
-
|
|
352
|
-
-- Add soft delete filter if enabled for this entity
|
|
353
|
-
IF l_entity_config.soft_delete THEN
|
|
354
|
-
l_where_clause := l_where_clause || ' AND t.deleted_at IS NULL';
|
|
355
|
-
END IF;
|
|
356
|
-
|
|
357
|
-
-- Build base SQL
|
|
358
|
-
l_base_sql := format('FROM %I t %s', p_entity, l_where_clause);
|
|
359
|
-
|
|
360
|
-
-- Get total count
|
|
361
|
-
l_count_sql := 'SELECT COUNT(*) ' || l_base_sql;
|
|
362
|
-
EXECUTE l_count_sql INTO l_total;
|
|
363
|
-
|
|
364
|
-
-- Get paginated data - Always use subquery to ensure LIMIT works correctly
|
|
365
|
-
IF l_order_clause != '' THEN
|
|
366
|
-
l_data_sql := format('SELECT COALESCE(jsonb_agg(to_jsonb(sub.*)), ''[]''::jsonb) FROM (SELECT t.* %s %s LIMIT %L OFFSET %L) sub',
|
|
367
|
-
l_base_sql, l_order_clause, l_limit, l_offset);
|
|
368
|
-
ELSE
|
|
369
|
-
-- Use subquery even without ORDER BY to ensure LIMIT is applied before aggregation
|
|
370
|
-
l_data_sql := format('SELECT COALESCE(jsonb_agg(to_jsonb(sub.*)), ''[]''::jsonb) FROM (SELECT t.* %s ORDER BY %s LIMIT %L OFFSET %L) sub',
|
|
371
|
-
l_base_sql, l_pk_order, l_limit, l_offset);
|
|
372
|
-
END IF;
|
|
373
|
-
|
|
374
|
-
EXECUTE l_data_sql INTO l_data;
|
|
375
|
-
|
|
376
|
-
-- Process FK dereferencing for each record
|
|
377
|
-
l_fk_includes := l_entity_config.fk_includes;
|
|
378
|
-
IF l_fk_includes IS NOT NULL AND l_fk_includes != '{}' AND l_data IS NOT NULL AND jsonb_array_length(l_data) > 0 THEN
|
|
379
|
-
-- Process each record in the data array
|
|
380
|
-
FOR i IN 0..jsonb_array_length(l_data) - 1 LOOP
|
|
381
|
-
l_record := l_data->i;
|
|
382
|
-
|
|
383
|
-
-- Dereference foreign keys for this record
|
|
384
|
-
FOR l_key, l_value IN SELECT key, value FROM jsonb_each_text(l_fk_includes)
|
|
385
|
-
LOOP
|
|
386
|
-
-- Handle different FK reference formats
|
|
387
|
-
IF l_value LIKE '%.%' THEN
|
|
388
|
-
-- Format: "table.field" for reverse foreign keys
|
|
389
|
-
l_fk_result := dzql.resolve_reverse_fk(l_record, l_key, l_value, l_on_date);
|
|
390
|
-
ELSIF l_key = l_value THEN
|
|
391
|
-
-- When key equals value (e.g., "sites": "sites"), it's a reverse FK
|
|
392
|
-
-- The target table has a field named {entity_singular}_id pointing back to this entity
|
|
393
|
-
-- Convert plural entity name to singular (simple rule: remove trailing 's')
|
|
394
|
-
l_fk_result := dzql.resolve_reverse_fk(l_record, l_key,
|
|
395
|
-
l_value || '.' || regexp_replace(p_entity, 's$', '') || '_id', l_on_date);
|
|
396
|
-
ELSE
|
|
397
|
-
-- Format: "table" for direct foreign keys
|
|
398
|
-
l_fk_result := dzql.resolve_direct_fk(l_record, l_key, l_value, l_on_date);
|
|
399
|
-
END IF;
|
|
400
|
-
|
|
401
|
-
IF l_fk_result IS NOT NULL THEN
|
|
402
|
-
l_record := l_record || jsonb_build_object(l_key, l_fk_result);
|
|
403
|
-
END IF;
|
|
404
|
-
END LOOP;
|
|
405
|
-
|
|
406
|
-
-- Expand many-to-many relationships for this record (if configured)
|
|
407
|
-
IF l_entity_config.many_to_many IS NOT NULL AND l_entity_config.many_to_many != '{}'::jsonb THEN
|
|
408
|
-
DECLARE
|
|
409
|
-
l_m2m_key text;
|
|
410
|
-
l_m2m_config jsonb;
|
|
411
|
-
l_id_field text;
|
|
412
|
-
l_junction_table text;
|
|
413
|
-
l_local_key text;
|
|
414
|
-
l_foreign_key text;
|
|
415
|
-
l_target_entity text;
|
|
416
|
-
l_expand boolean;
|
|
417
|
-
l_record_id text;
|
|
418
|
-
l_id_array jsonb;
|
|
419
|
-
l_expanded_objects jsonb;
|
|
420
|
-
l_pk_cols text[];
|
|
421
|
-
BEGIN
|
|
422
|
-
-- Get primary key columns for this entity
|
|
423
|
-
SELECT array_agg(a.attname ORDER BY a.attnum)
|
|
424
|
-
INTO l_pk_cols
|
|
425
|
-
FROM pg_index idx
|
|
426
|
-
JOIN pg_attribute a ON a.attrelid = idx.indrelid AND a.attnum = ANY(idx.indkey)
|
|
427
|
-
WHERE idx.indrelid = p_entity::regclass AND idx.indisprimary;
|
|
428
|
-
|
|
429
|
-
-- Get the primary key value from the record
|
|
430
|
-
l_record_id := l_record->>l_pk_cols[1]; -- Assume single PK for now
|
|
431
|
-
|
|
432
|
-
FOR l_m2m_key IN SELECT jsonb_object_keys(l_entity_config.many_to_many)
|
|
433
|
-
LOOP
|
|
434
|
-
l_m2m_config := l_entity_config.many_to_many->l_m2m_key;
|
|
435
|
-
l_id_field := l_m2m_config->>'id_field';
|
|
436
|
-
l_junction_table := l_m2m_config->>'junction_table';
|
|
437
|
-
l_local_key := l_m2m_config->>'local_key';
|
|
438
|
-
l_foreign_key := l_m2m_config->>'foreign_key';
|
|
439
|
-
l_target_entity := l_m2m_config->>'target_entity';
|
|
440
|
-
l_expand := COALESCE((l_m2m_config->>'expand')::boolean, false);
|
|
441
|
-
|
|
442
|
-
-- Always include array of IDs
|
|
443
|
-
EXECUTE format('
|
|
444
|
-
SELECT COALESCE(jsonb_agg(%I), ''[]''::jsonb)
|
|
445
|
-
FROM %I
|
|
446
|
-
WHERE %I = $1::int
|
|
447
|
-
', l_foreign_key, l_junction_table, l_local_key)
|
|
448
|
-
INTO l_id_array
|
|
449
|
-
USING l_record_id;
|
|
450
|
-
|
|
451
|
-
l_record := l_record || jsonb_build_object(l_id_field, l_id_array);
|
|
452
|
-
|
|
453
|
-
-- Conditionally include expanded objects if expand: true
|
|
454
|
-
IF l_expand THEN
|
|
455
|
-
EXECUTE format('
|
|
456
|
-
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
457
|
-
FROM %I jt
|
|
458
|
-
JOIN %I t ON t.id = jt.%I
|
|
459
|
-
WHERE jt.%I = $1::int
|
|
460
|
-
', l_junction_table, l_target_entity, l_foreign_key, l_local_key)
|
|
461
|
-
INTO l_expanded_objects
|
|
462
|
-
USING l_record_id;
|
|
463
|
-
|
|
464
|
-
l_record := l_record || jsonb_build_object(l_m2m_key, l_expanded_objects);
|
|
465
|
-
END IF;
|
|
466
|
-
END LOOP;
|
|
467
|
-
END;
|
|
468
|
-
END IF;
|
|
469
|
-
|
|
470
|
-
l_processed_data := l_processed_data || l_record;
|
|
471
|
-
END LOOP;
|
|
472
|
-
|
|
473
|
-
-- Convert processed data back to jsonb array
|
|
474
|
-
l_data := to_jsonb(l_processed_data);
|
|
475
|
-
END IF;
|
|
476
|
-
|
|
477
|
-
RETURN jsonb_build_object(
|
|
478
|
-
'data', COALESCE(l_data, '[]'::jsonb),
|
|
479
|
-
'total', l_total,
|
|
480
|
-
'page', l_page,
|
|
481
|
-
'limit', l_limit,
|
|
482
|
-
'pages', CEIL(l_total::numeric / l_limit)
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
EXCEPTION
|
|
486
|
-
WHEN OTHERS THEN
|
|
487
|
-
RAISE EXCEPTION 'DZQL: search error for entity %: %', p_entity, SQLERRM;
|
|
488
|
-
END $$;
|
|
489
|
-
|
|
490
|
-
-- ============================================================================
|
|
491
|
-
-- SEARCH UTILITIES
|
|
492
|
-
-- ============================================================================
|
|
493
|
-
|
|
494
|
-
-- Build faceted search aggregations
|
|
495
|
-
CREATE OR REPLACE FUNCTION dzql.build_search_facets(
|
|
496
|
-
p_entity text,
|
|
497
|
-
p_filters jsonb,
|
|
498
|
-
p_facet_fields text[]
|
|
499
|
-
) RETURNS jsonb
|
|
500
|
-
LANGUAGE plpgsql AS $$
|
|
501
|
-
DECLARE
|
|
502
|
-
l_base_where text;
|
|
503
|
-
l_facet_field text;
|
|
504
|
-
l_facet_sql text;
|
|
505
|
-
l_facet_result jsonb;
|
|
506
|
-
l_facets jsonb := '{}'::jsonb;
|
|
507
|
-
BEGIN
|
|
508
|
-
-- Build base WHERE clause (without the facet field being aggregated)
|
|
509
|
-
l_base_where := COALESCE(dzql.build_where_clause(p_entity, p_filters), '1=1');
|
|
510
|
-
|
|
511
|
-
-- Build facets for each requested field
|
|
512
|
-
FOREACH l_facet_field IN ARRAY p_facet_fields
|
|
513
|
-
LOOP
|
|
514
|
-
l_facet_sql := format(
|
|
515
|
-
'SELECT COALESCE(jsonb_agg(jsonb_build_object(''value'', %I, ''count'', count)), ''[]''::jsonb)
|
|
516
|
-
FROM (
|
|
517
|
-
SELECT %I, COUNT(*) as count
|
|
518
|
-
FROM %I
|
|
519
|
-
WHERE %s AND %I IS NOT NULL
|
|
520
|
-
GROUP BY %I
|
|
521
|
-
ORDER BY count DESC, %I
|
|
522
|
-
LIMIT 20
|
|
523
|
-
) facet_data',
|
|
524
|
-
l_facet_field, l_facet_field, p_entity, l_base_where,
|
|
525
|
-
l_facet_field, l_facet_field, l_facet_field
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
EXECUTE l_facet_sql INTO l_facet_result;
|
|
529
|
-
l_facets := l_facets || jsonb_build_object(l_facet_field, l_facet_result);
|
|
530
|
-
END LOOP;
|
|
531
|
-
|
|
532
|
-
RETURN l_facets;
|
|
533
|
-
END $$;
|
|
534
|
-
|
|
535
|
-
-- Build search suggestions based on searchable fields
|
|
536
|
-
CREATE OR REPLACE FUNCTION dzql.build_search_suggestions(
|
|
537
|
-
p_entity text,
|
|
538
|
-
p_partial_text text,
|
|
539
|
-
p_limit int DEFAULT 10
|
|
540
|
-
) RETURNS jsonb
|
|
541
|
-
LANGUAGE plpgsql AS $$
|
|
542
|
-
DECLARE
|
|
543
|
-
l_entity_config record;
|
|
544
|
-
l_field text;
|
|
545
|
-
l_suggestions_sql text;
|
|
546
|
-
l_field_sqls text[] := array[]::text[];
|
|
547
|
-
l_result jsonb;
|
|
548
|
-
BEGIN
|
|
549
|
-
-- Get entity configuration
|
|
550
|
-
SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_entity;
|
|
551
|
-
|
|
552
|
-
IF l_entity_config IS NULL THEN
|
|
553
|
-
RETURN '[]'::jsonb;
|
|
554
|
-
END IF;
|
|
555
|
-
|
|
556
|
-
-- Build UNION query for all searchable fields
|
|
557
|
-
FOREACH l_field IN ARRAY l_entity_config.searchable_fields
|
|
558
|
-
LOOP
|
|
559
|
-
l_field_sqls := l_field_sqls || format(
|
|
560
|
-
'SELECT DISTINCT %I::text as suggestion FROM %I WHERE %I::text ILIKE %L AND %I IS NOT NULL',
|
|
561
|
-
l_field, p_entity, l_field, p_partial_text || '%', l_field
|
|
562
|
-
);
|
|
563
|
-
END LOOP;
|
|
564
|
-
|
|
565
|
-
IF array_length(l_field_sqls, 1) = 0 THEN
|
|
566
|
-
RETURN '[]'::jsonb;
|
|
567
|
-
END IF;
|
|
568
|
-
|
|
569
|
-
l_suggestions_sql := format(
|
|
570
|
-
'SELECT COALESCE(jsonb_agg(suggestion ORDER BY suggestion), ''[]''::jsonb) FROM (
|
|
571
|
-
%s
|
|
572
|
-
LIMIT %s
|
|
573
|
-
) suggestions',
|
|
574
|
-
array_to_string(l_field_sqls, ' UNION '),
|
|
575
|
-
p_limit
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
EXECUTE l_suggestions_sql INTO l_result;
|
|
579
|
-
|
|
580
|
-
RETURN COALESCE(l_result, '[]'::jsonb);
|
|
581
|
-
END $$;
|