@biref/scanner 0.0.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/LICENSE +21 -0
- package/README.md +1109 -0
- package/bin/biref.mjs +7 -0
- package/dist/codegen/cli.js +7435 -0
- package/dist/codegen/cli.js.map +1 -0
- package/dist/index.cjs +2271 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1507 -0
- package/dist/index.d.ts +1507 -0
- package/dist/index.js +2248 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/adapters/postgres/pgEnums.ts
|
|
4
|
+
var PgTypeCategory = {
|
|
5
|
+
Array: "A"};
|
|
6
|
+
var PgTypeKind = {
|
|
7
|
+
Enum: "e"};
|
|
8
|
+
var PgConstraintKind = {
|
|
9
|
+
Check: "c",
|
|
10
|
+
Unique: "u",
|
|
11
|
+
Exclusion: "x"
|
|
12
|
+
};
|
|
13
|
+
var PgReferentialAction = {
|
|
14
|
+
NoAction: "a",
|
|
15
|
+
Restrict: "r",
|
|
16
|
+
Cascade: "c",
|
|
17
|
+
SetNull: "n",
|
|
18
|
+
SetDefault: "d"
|
|
19
|
+
};
|
|
20
|
+
var PgIndexMethod = {
|
|
21
|
+
BTree: "btree",
|
|
22
|
+
Hash: "hash",
|
|
23
|
+
Gin: "gin",
|
|
24
|
+
Gist: "gist",
|
|
25
|
+
Brin: "brin",
|
|
26
|
+
SpGist: "spgist"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/adapters/postgres/mapping/ConstraintMapper.ts
|
|
30
|
+
var ConstraintMapper = class _ConstraintMapper {
|
|
31
|
+
static toConstraint(raw) {
|
|
32
|
+
return {
|
|
33
|
+
name: raw.name,
|
|
34
|
+
kind: _ConstraintMapper.toKind(raw.kind_code),
|
|
35
|
+
fields: raw.columns,
|
|
36
|
+
expression: raw.definition
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
static toKind(code) {
|
|
40
|
+
switch (code) {
|
|
41
|
+
case PgConstraintKind.Check: {
|
|
42
|
+
return "check";
|
|
43
|
+
}
|
|
44
|
+
case PgConstraintKind.Unique: {
|
|
45
|
+
return "unique";
|
|
46
|
+
}
|
|
47
|
+
case PgConstraintKind.Exclusion: {
|
|
48
|
+
return "exclusion";
|
|
49
|
+
}
|
|
50
|
+
default: {
|
|
51
|
+
return "custom";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/adapters/postgres/mapping/FieldMapper.ts
|
|
58
|
+
var FieldMapper = class _FieldMapper {
|
|
59
|
+
static toField(col, isIdentifier) {
|
|
60
|
+
return {
|
|
61
|
+
name: col.name,
|
|
62
|
+
type: _FieldMapper.toType(col),
|
|
63
|
+
nullable: col.nullable,
|
|
64
|
+
isIdentifier,
|
|
65
|
+
defaultValue: col.default_value,
|
|
66
|
+
description: col.description
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
static toType(col) {
|
|
70
|
+
if (col.type_category === PgTypeCategory.Array) {
|
|
71
|
+
return {
|
|
72
|
+
category: "array",
|
|
73
|
+
nativeType: col.native_type,
|
|
74
|
+
elementType: _FieldMapper.toElementType(col)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (col.type_kind === PgTypeKind.Enum) {
|
|
78
|
+
return {
|
|
79
|
+
category: "enum",
|
|
80
|
+
nativeType: col.native_type,
|
|
81
|
+
enumValues: col.enum_values ?? []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
category: _FieldMapper.categorize(col.udt_name),
|
|
86
|
+
nativeType: col.native_type
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
static toElementType(col) {
|
|
90
|
+
const elementUdt = col.element_udt_name ?? (col.udt_name.startsWith("_") ? col.udt_name.slice(1) : col.udt_name);
|
|
91
|
+
if (col.element_type_kind === PgTypeKind.Enum) {
|
|
92
|
+
return {
|
|
93
|
+
category: "enum",
|
|
94
|
+
nativeType: elementUdt,
|
|
95
|
+
enumValues: col.element_enum_values ?? []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
category: _FieldMapper.categorize(elementUdt),
|
|
100
|
+
nativeType: elementUdt
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
static categorize(udt) {
|
|
104
|
+
switch (udt.toLowerCase()) {
|
|
105
|
+
case "varchar":
|
|
106
|
+
case "bpchar":
|
|
107
|
+
case "char":
|
|
108
|
+
case "text":
|
|
109
|
+
case "name":
|
|
110
|
+
case "citext": {
|
|
111
|
+
return "string";
|
|
112
|
+
}
|
|
113
|
+
case "int2":
|
|
114
|
+
case "int4":
|
|
115
|
+
case "int8":
|
|
116
|
+
case "smallint":
|
|
117
|
+
case "integer":
|
|
118
|
+
case "bigint": {
|
|
119
|
+
return "integer";
|
|
120
|
+
}
|
|
121
|
+
case "numeric":
|
|
122
|
+
case "decimal":
|
|
123
|
+
case "float4":
|
|
124
|
+
case "float8":
|
|
125
|
+
case "real":
|
|
126
|
+
case "money": {
|
|
127
|
+
return "decimal";
|
|
128
|
+
}
|
|
129
|
+
case "bool":
|
|
130
|
+
case "boolean": {
|
|
131
|
+
return "boolean";
|
|
132
|
+
}
|
|
133
|
+
case "date": {
|
|
134
|
+
return "date";
|
|
135
|
+
}
|
|
136
|
+
case "timestamp":
|
|
137
|
+
case "timestamptz": {
|
|
138
|
+
return "timestamp";
|
|
139
|
+
}
|
|
140
|
+
case "time":
|
|
141
|
+
case "timetz": {
|
|
142
|
+
return "time";
|
|
143
|
+
}
|
|
144
|
+
case "json":
|
|
145
|
+
case "jsonb": {
|
|
146
|
+
return "json";
|
|
147
|
+
}
|
|
148
|
+
case "uuid": {
|
|
149
|
+
return "uuid";
|
|
150
|
+
}
|
|
151
|
+
case "bytea": {
|
|
152
|
+
return "binary";
|
|
153
|
+
}
|
|
154
|
+
default: {
|
|
155
|
+
return "unknown";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/adapters/postgres/mapping/IndexMapper.ts
|
|
162
|
+
var IndexMapper = class _IndexMapper {
|
|
163
|
+
static toIndex(raw) {
|
|
164
|
+
return {
|
|
165
|
+
name: raw.name,
|
|
166
|
+
fields: raw.columns,
|
|
167
|
+
unique: raw.unique,
|
|
168
|
+
kind: _IndexMapper.toKind(raw.method),
|
|
169
|
+
partial: raw.partial,
|
|
170
|
+
definition: raw.definition
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
static toKind(method) {
|
|
174
|
+
switch (method.toLowerCase()) {
|
|
175
|
+
case PgIndexMethod.BTree: {
|
|
176
|
+
return "btree";
|
|
177
|
+
}
|
|
178
|
+
case PgIndexMethod.Hash: {
|
|
179
|
+
return "hash";
|
|
180
|
+
}
|
|
181
|
+
case PgIndexMethod.Gin: {
|
|
182
|
+
return "gin";
|
|
183
|
+
}
|
|
184
|
+
case PgIndexMethod.Gist: {
|
|
185
|
+
return "gist";
|
|
186
|
+
}
|
|
187
|
+
case PgIndexMethod.Brin: {
|
|
188
|
+
return "brin";
|
|
189
|
+
}
|
|
190
|
+
case PgIndexMethod.SpGist: {
|
|
191
|
+
return "spgist";
|
|
192
|
+
}
|
|
193
|
+
default: {
|
|
194
|
+
return "unknown";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// src/adapters/postgres/mapping/ReferenceMapper.ts
|
|
201
|
+
var ReferenceMapper = class _ReferenceMapper {
|
|
202
|
+
static toReference(fk) {
|
|
203
|
+
return {
|
|
204
|
+
name: fk.name,
|
|
205
|
+
fromEntity: { namespace: fk.from_namespace, name: fk.from_table },
|
|
206
|
+
fromFields: fk.from_columns,
|
|
207
|
+
toEntity: { namespace: fk.to_namespace, name: fk.to_table },
|
|
208
|
+
toFields: fk.to_columns,
|
|
209
|
+
confidence: 1,
|
|
210
|
+
onUpdate: _ReferenceMapper.toAction(fk.update_action_code),
|
|
211
|
+
onDelete: _ReferenceMapper.toAction(fk.delete_action_code)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
static toAction(code) {
|
|
215
|
+
switch (code) {
|
|
216
|
+
case PgReferentialAction.NoAction: {
|
|
217
|
+
return "no-action";
|
|
218
|
+
}
|
|
219
|
+
case PgReferentialAction.Restrict: {
|
|
220
|
+
return "restrict";
|
|
221
|
+
}
|
|
222
|
+
case PgReferentialAction.Cascade: {
|
|
223
|
+
return "cascade";
|
|
224
|
+
}
|
|
225
|
+
case PgReferentialAction.SetNull: {
|
|
226
|
+
return "set-null";
|
|
227
|
+
}
|
|
228
|
+
case PgReferentialAction.SetDefault: {
|
|
229
|
+
return "set-default";
|
|
230
|
+
}
|
|
231
|
+
default: {
|
|
232
|
+
return "no-action";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/adapters/postgres/mapping/EntityAssembler.ts
|
|
239
|
+
var EntityAssembler = class _EntityAssembler {
|
|
240
|
+
static assemble(inputs) {
|
|
241
|
+
const tableKeys = new Set(
|
|
242
|
+
inputs.tables.map((t) => _EntityAssembler.qualified(t.namespace, t.name))
|
|
243
|
+
);
|
|
244
|
+
const pkByEntity = _EntityAssembler.indexPrimaryKeys(inputs.primaryKeys);
|
|
245
|
+
const colsByEntity = _EntityAssembler.indexColumns(inputs.columns);
|
|
246
|
+
const relsByEntity = _EntityAssembler.buildRelationships(
|
|
247
|
+
inputs.foreignKeys,
|
|
248
|
+
tableKeys
|
|
249
|
+
);
|
|
250
|
+
const indexesByEntity = _EntityAssembler.buildIndexes(
|
|
251
|
+
inputs.indexes,
|
|
252
|
+
tableKeys
|
|
253
|
+
);
|
|
254
|
+
const constraintsByEntity = _EntityAssembler.buildConstraints(
|
|
255
|
+
inputs.constraints,
|
|
256
|
+
tableKeys
|
|
257
|
+
);
|
|
258
|
+
return inputs.tables.map((table) => {
|
|
259
|
+
const key = _EntityAssembler.qualified(table.namespace, table.name);
|
|
260
|
+
const pkColumns = pkByEntity.get(key) ?? [];
|
|
261
|
+
const pkSet = new Set(pkColumns);
|
|
262
|
+
const rawCols = colsByEntity.get(key) ?? [];
|
|
263
|
+
const fields = rawCols.map(
|
|
264
|
+
(col) => FieldMapper.toField(col, pkSet.has(col.name))
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
namespace: table.namespace,
|
|
268
|
+
name: table.name,
|
|
269
|
+
fields,
|
|
270
|
+
identifier: pkColumns,
|
|
271
|
+
relationships: relsByEntity.get(key) ?? [],
|
|
272
|
+
constraints: constraintsByEntity.get(key) ?? [],
|
|
273
|
+
indexes: indexesByEntity.get(key) ?? [],
|
|
274
|
+
description: table.description
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
static indexPrimaryKeys(primaryKeys) {
|
|
279
|
+
const map = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const pk of primaryKeys) {
|
|
281
|
+
map.set(
|
|
282
|
+
_EntityAssembler.qualified(pk.namespace, pk.table_name),
|
|
283
|
+
pk.columns
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return map;
|
|
287
|
+
}
|
|
288
|
+
static indexColumns(columns) {
|
|
289
|
+
const map = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const col of columns) {
|
|
291
|
+
const key = _EntityAssembler.qualified(col.namespace, col.table_name);
|
|
292
|
+
const list = map.get(key);
|
|
293
|
+
if (list) {
|
|
294
|
+
list.push(col);
|
|
295
|
+
} else {
|
|
296
|
+
map.set(key, [col]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return map;
|
|
300
|
+
}
|
|
301
|
+
static buildRelationships(foreignKeys, tableKeys) {
|
|
302
|
+
const map = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const fk of foreignKeys) {
|
|
304
|
+
const reference = ReferenceMapper.toReference(fk);
|
|
305
|
+
const fromKey = _EntityAssembler.qualified(
|
|
306
|
+
fk.from_namespace,
|
|
307
|
+
fk.from_table
|
|
308
|
+
);
|
|
309
|
+
const toKey = _EntityAssembler.qualified(fk.to_namespace, fk.to_table);
|
|
310
|
+
if (tableKeys.has(fromKey)) {
|
|
311
|
+
_EntityAssembler.push(map, fromKey, {
|
|
312
|
+
direction: "outbound",
|
|
313
|
+
reference
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (tableKeys.has(toKey)) {
|
|
317
|
+
_EntityAssembler.push(map, toKey, {
|
|
318
|
+
direction: "inbound",
|
|
319
|
+
reference
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return map;
|
|
324
|
+
}
|
|
325
|
+
static buildIndexes(indexes, tableKeys) {
|
|
326
|
+
const map = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const idx of indexes) {
|
|
328
|
+
const key = _EntityAssembler.qualified(idx.namespace, idx.table_name);
|
|
329
|
+
if (!tableKeys.has(key)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
_EntityAssembler.push(map, key, IndexMapper.toIndex(idx));
|
|
333
|
+
}
|
|
334
|
+
return map;
|
|
335
|
+
}
|
|
336
|
+
static buildConstraints(constraints, tableKeys) {
|
|
337
|
+
const map = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const cn of constraints) {
|
|
339
|
+
const key = _EntityAssembler.qualified(cn.namespace, cn.table_name);
|
|
340
|
+
if (!tableKeys.has(key)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
_EntityAssembler.push(map, key, ConstraintMapper.toConstraint(cn));
|
|
344
|
+
}
|
|
345
|
+
return map;
|
|
346
|
+
}
|
|
347
|
+
static push(map, key, value) {
|
|
348
|
+
const list = map.get(key);
|
|
349
|
+
if (list) {
|
|
350
|
+
list.push(value);
|
|
351
|
+
} else {
|
|
352
|
+
map.set(key, [value]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
static qualified(namespace, name) {
|
|
356
|
+
return `${namespace}.${name}`;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/adapters/postgres/PostgresMeta.ts
|
|
361
|
+
var POSTGRES_ADAPTER_NAME = "postgres";
|
|
362
|
+
var POSTGRES_URL_SCHEMES = [
|
|
363
|
+
"postgres",
|
|
364
|
+
"postgresql"
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
// src/adapters/postgres/queries/ColumnsQuery.ts
|
|
368
|
+
var ColumnsQuery = class _ColumnsQuery {
|
|
369
|
+
static SQL = `
|
|
370
|
+
SELECT
|
|
371
|
+
n.nspname AS namespace,
|
|
372
|
+
c.relname AS table_name,
|
|
373
|
+
a.attname AS name,
|
|
374
|
+
format_type(a.atttypid, a.atttypmod) AS native_type,
|
|
375
|
+
t.typname AS udt_name,
|
|
376
|
+
t.typcategory AS type_category,
|
|
377
|
+
t.typtype AS type_kind,
|
|
378
|
+
NOT a.attnotnull AS nullable,
|
|
379
|
+
pg_get_expr(d.adbin, d.adrelid) AS default_value,
|
|
380
|
+
col_description(c.oid, a.attnum) AS description,
|
|
381
|
+
CASE
|
|
382
|
+
WHEN t.typtype = 'e' THEN (
|
|
383
|
+
SELECT array_agg(enumlabel ORDER BY enumsortorder)::text[]
|
|
384
|
+
FROM pg_enum
|
|
385
|
+
WHERE enumtypid = a.atttypid
|
|
386
|
+
)
|
|
387
|
+
ELSE NULL
|
|
388
|
+
END AS enum_values,
|
|
389
|
+
et.typname AS element_udt_name,
|
|
390
|
+
et.typcategory AS element_type_category,
|
|
391
|
+
et.typtype AS element_type_kind,
|
|
392
|
+
CASE
|
|
393
|
+
WHEN et.typtype = 'e' THEN (
|
|
394
|
+
SELECT array_agg(enumlabel ORDER BY enumsortorder)::text[]
|
|
395
|
+
FROM pg_enum
|
|
396
|
+
WHERE enumtypid = et.oid
|
|
397
|
+
)
|
|
398
|
+
ELSE NULL
|
|
399
|
+
END AS element_enum_values
|
|
400
|
+
FROM pg_attribute a
|
|
401
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
402
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
403
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
404
|
+
LEFT JOIN pg_type et ON et.oid = t.typelem AND t.typcategory = 'A'
|
|
405
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
|
|
406
|
+
WHERE c.relkind IN ('r', 'p')
|
|
407
|
+
AND a.attnum > 0
|
|
408
|
+
AND NOT a.attisdropped
|
|
409
|
+
AND n.nspname = ANY($1)
|
|
410
|
+
ORDER BY n.nspname, c.relname, a.attnum
|
|
411
|
+
`;
|
|
412
|
+
static async fetch(client, namespaces) {
|
|
413
|
+
const result = await client.query(_ColumnsQuery.SQL, [
|
|
414
|
+
namespaces
|
|
415
|
+
]);
|
|
416
|
+
return result.rows;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// src/adapters/postgres/queries/ConstraintsQuery.ts
|
|
421
|
+
var ConstraintsQuery = class _ConstraintsQuery {
|
|
422
|
+
static SQL = `
|
|
423
|
+
SELECT
|
|
424
|
+
n.nspname AS namespace,
|
|
425
|
+
cls.relname AS table_name,
|
|
426
|
+
c.conname AS name,
|
|
427
|
+
c.contype AS kind_code,
|
|
428
|
+
pg_get_constraintdef(c.oid) AS definition,
|
|
429
|
+
CASE
|
|
430
|
+
WHEN array_length(c.conkey, 1) > 0 THEN
|
|
431
|
+
(SELECT array_agg(att.attname ORDER BY u.ord)::text[]
|
|
432
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord)
|
|
433
|
+
JOIN pg_attribute att
|
|
434
|
+
ON att.attrelid = c.conrelid AND att.attnum = u.attnum)
|
|
435
|
+
ELSE ARRAY[]::text[]
|
|
436
|
+
END AS columns
|
|
437
|
+
FROM pg_constraint c
|
|
438
|
+
JOIN pg_class cls ON cls.oid = c.conrelid
|
|
439
|
+
JOIN pg_namespace n ON n.oid = cls.relnamespace
|
|
440
|
+
WHERE c.contype IN ('c', 'u', 'x')
|
|
441
|
+
AND n.nspname = ANY($1)
|
|
442
|
+
ORDER BY n.nspname, cls.relname, c.conname
|
|
443
|
+
`;
|
|
444
|
+
static async fetch(client, namespaces) {
|
|
445
|
+
const result = await client.query(_ConstraintsQuery.SQL, [
|
|
446
|
+
namespaces
|
|
447
|
+
]);
|
|
448
|
+
return result.rows;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// src/adapters/postgres/queries/ForeignKeysQuery.ts
|
|
453
|
+
var ForeignKeysQuery = class _ForeignKeysQuery {
|
|
454
|
+
static SQL = `
|
|
455
|
+
SELECT
|
|
456
|
+
c.conname AS name,
|
|
457
|
+
fn.nspname AS from_namespace,
|
|
458
|
+
fc.relname AS from_table,
|
|
459
|
+
(SELECT array_agg(att.attname ORDER BY u.ord)::text[]
|
|
460
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord)
|
|
461
|
+
JOIN pg_attribute att
|
|
462
|
+
ON att.attrelid = c.conrelid AND att.attnum = u.attnum
|
|
463
|
+
) AS from_columns,
|
|
464
|
+
tn.nspname AS to_namespace,
|
|
465
|
+
tc.relname AS to_table,
|
|
466
|
+
(SELECT array_agg(att.attname ORDER BY u.ord)::text[]
|
|
467
|
+
FROM unnest(c.confkey) WITH ORDINALITY AS u(attnum, ord)
|
|
468
|
+
JOIN pg_attribute att
|
|
469
|
+
ON att.attrelid = c.confrelid AND att.attnum = u.attnum
|
|
470
|
+
) AS to_columns,
|
|
471
|
+
c.confupdtype AS update_action_code,
|
|
472
|
+
c.confdeltype AS delete_action_code
|
|
473
|
+
FROM pg_constraint c
|
|
474
|
+
JOIN pg_class fc ON fc.oid = c.conrelid
|
|
475
|
+
JOIN pg_namespace fn ON fn.oid = fc.relnamespace
|
|
476
|
+
JOIN pg_class tc ON tc.oid = c.confrelid
|
|
477
|
+
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
|
478
|
+
WHERE c.contype = 'f'
|
|
479
|
+
AND fn.nspname = ANY($1)
|
|
480
|
+
`;
|
|
481
|
+
static async fetch(client, namespaces) {
|
|
482
|
+
const result = await client.query(_ForeignKeysQuery.SQL, [
|
|
483
|
+
namespaces
|
|
484
|
+
]);
|
|
485
|
+
return result.rows;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// src/adapters/postgres/queries/IndexesQuery.ts
|
|
490
|
+
var IndexesQuery = class _IndexesQuery {
|
|
491
|
+
static SQL = `
|
|
492
|
+
SELECT
|
|
493
|
+
n.nspname AS namespace,
|
|
494
|
+
t.relname AS table_name,
|
|
495
|
+
i.relname AS name,
|
|
496
|
+
am.amname AS method,
|
|
497
|
+
ix.indisunique AS unique,
|
|
498
|
+
(ix.indpred IS NOT NULL) AS partial,
|
|
499
|
+
pg_get_indexdef(ix.indexrelid) AS definition,
|
|
500
|
+
ARRAY(
|
|
501
|
+
SELECT a.attname
|
|
502
|
+
FROM unnest(ix.indkey::int[]) WITH ORDINALITY AS k(attnum, ord)
|
|
503
|
+
JOIN pg_attribute a
|
|
504
|
+
ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
505
|
+
ORDER BY k.ord
|
|
506
|
+
)::text[] AS columns
|
|
507
|
+
FROM pg_index ix
|
|
508
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
509
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
510
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
511
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
512
|
+
WHERE NOT ix.indisprimary
|
|
513
|
+
AND n.nspname = ANY($1)
|
|
514
|
+
ORDER BY n.nspname, t.relname, i.relname
|
|
515
|
+
`;
|
|
516
|
+
static async fetch(client, namespaces) {
|
|
517
|
+
const result = await client.query(_IndexesQuery.SQL, [namespaces]);
|
|
518
|
+
return result.rows;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/adapters/postgres/queries/PrimaryKeysQuery.ts
|
|
523
|
+
var PrimaryKeysQuery = class _PrimaryKeysQuery {
|
|
524
|
+
static SQL = `
|
|
525
|
+
SELECT
|
|
526
|
+
n.nspname AS namespace,
|
|
527
|
+
cls.relname AS table_name,
|
|
528
|
+
(SELECT array_agg(att.attname ORDER BY u.ord)::text[]
|
|
529
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord)
|
|
530
|
+
JOIN pg_attribute att
|
|
531
|
+
ON att.attrelid = c.conrelid AND att.attnum = u.attnum
|
|
532
|
+
) AS columns
|
|
533
|
+
FROM pg_constraint c
|
|
534
|
+
JOIN pg_class cls ON cls.oid = c.conrelid
|
|
535
|
+
JOIN pg_namespace n ON n.oid = cls.relnamespace
|
|
536
|
+
WHERE c.contype = 'p'
|
|
537
|
+
AND n.nspname = ANY($1)
|
|
538
|
+
`;
|
|
539
|
+
static async fetch(client, namespaces) {
|
|
540
|
+
const result = await client.query(_PrimaryKeysQuery.SQL, [
|
|
541
|
+
namespaces
|
|
542
|
+
]);
|
|
543
|
+
return result.rows;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// src/adapters/postgres/queries/TablesQuery.ts
|
|
548
|
+
var TablesQuery = class _TablesQuery {
|
|
549
|
+
static SQL = `
|
|
550
|
+
SELECT
|
|
551
|
+
n.nspname AS namespace,
|
|
552
|
+
c.relname AS name,
|
|
553
|
+
obj_description(c.oid, 'pg_class') AS description
|
|
554
|
+
FROM pg_class c
|
|
555
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
556
|
+
WHERE c.relkind IN ('r', 'p')
|
|
557
|
+
AND n.nspname = ANY($1)
|
|
558
|
+
ORDER BY n.nspname, c.relname
|
|
559
|
+
`;
|
|
560
|
+
static async fetch(client, namespaces) {
|
|
561
|
+
const result = await client.query(_TablesQuery.SQL, [namespaces]);
|
|
562
|
+
return result.rows;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/domain/model/DataModel.ts
|
|
567
|
+
var DataModel = class _DataModel {
|
|
568
|
+
constructor(kind, entities) {
|
|
569
|
+
this.kind = kind;
|
|
570
|
+
this.entities = entities;
|
|
571
|
+
this.index = new Map(
|
|
572
|
+
entities.map((e) => [_DataModel.qualifiedName(e.namespace, e.name), e])
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
kind;
|
|
576
|
+
entities;
|
|
577
|
+
index;
|
|
578
|
+
/**
|
|
579
|
+
* Build the lookup key for an entity. Exposed as a static helper so
|
|
580
|
+
* consumers can compute keys consistently when building their own
|
|
581
|
+
* indexes on top of a `DataModel`.
|
|
582
|
+
*/
|
|
583
|
+
static qualifiedName(namespace, name) {
|
|
584
|
+
return `${namespace}.${name}`;
|
|
585
|
+
}
|
|
586
|
+
getEntity(namespace, name) {
|
|
587
|
+
return this.index.get(_DataModel.qualifiedName(namespace, name));
|
|
588
|
+
}
|
|
589
|
+
hasEntity(namespace, name) {
|
|
590
|
+
return this.index.has(_DataModel.qualifiedName(namespace, name));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* All relationships an entity participates in, in both directions.
|
|
594
|
+
* Returns an empty array if the entity is unknown.
|
|
595
|
+
*/
|
|
596
|
+
relationshipsOf(namespace, name) {
|
|
597
|
+
return this.getEntity(namespace, name)?.relationships ?? [];
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Outbound relationships: this entity holds the reference.
|
|
601
|
+
* In a relational store these correspond to FOREIGN KEY constraints
|
|
602
|
+
* declared by the entity itself.
|
|
603
|
+
*/
|
|
604
|
+
outboundRelationshipsOf(namespace, name) {
|
|
605
|
+
return this.relationshipsOf(namespace, name).filter(
|
|
606
|
+
(r) => r.direction === "outbound"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Inbound relationships: other entities hold references pointing here.
|
|
611
|
+
*
|
|
612
|
+
* This is the differentiating feature of the SDK. Most introspection
|
|
613
|
+
* tools only surface outbound relationships, requiring consumers to
|
|
614
|
+
* know which other entities reference theirs ahead of time.
|
|
615
|
+
*/
|
|
616
|
+
inboundRelationshipsOf(namespace, name) {
|
|
617
|
+
return this.relationshipsOf(namespace, name).filter(
|
|
618
|
+
(r) => r.direction === "inbound"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/adapters/postgres/PostgresIntrospector.ts
|
|
624
|
+
var DEFAULT_NAMESPACES = ["public"];
|
|
625
|
+
var PostgresIntrospector = class _PostgresIntrospector {
|
|
626
|
+
constructor(client) {
|
|
627
|
+
this.client = client;
|
|
628
|
+
}
|
|
629
|
+
client;
|
|
630
|
+
name = POSTGRES_ADAPTER_NAME;
|
|
631
|
+
kind = "relational";
|
|
632
|
+
async introspect(options = {}) {
|
|
633
|
+
const namespaces = await this.resolveNamespaces(options.namespaces);
|
|
634
|
+
const [tables, columns, primaryKeys, foreignKeys, indexes, constraints] = await Promise.all([
|
|
635
|
+
TablesQuery.fetch(this.client, namespaces),
|
|
636
|
+
ColumnsQuery.fetch(this.client, namespaces),
|
|
637
|
+
PrimaryKeysQuery.fetch(this.client, namespaces),
|
|
638
|
+
ForeignKeysQuery.fetch(this.client, namespaces),
|
|
639
|
+
IndexesQuery.fetch(this.client, namespaces),
|
|
640
|
+
ConstraintsQuery.fetch(this.client, namespaces)
|
|
641
|
+
]);
|
|
642
|
+
const filteredTables = _PostgresIntrospector.applyEntityFilters(
|
|
643
|
+
tables,
|
|
644
|
+
options
|
|
645
|
+
);
|
|
646
|
+
const entities = EntityAssembler.assemble({
|
|
647
|
+
tables: filteredTables,
|
|
648
|
+
columns,
|
|
649
|
+
primaryKeys,
|
|
650
|
+
foreignKeys,
|
|
651
|
+
indexes,
|
|
652
|
+
constraints
|
|
653
|
+
});
|
|
654
|
+
return new DataModel("relational", entities);
|
|
655
|
+
}
|
|
656
|
+
async resolveNamespaces(raw) {
|
|
657
|
+
if (raw === void 0) {
|
|
658
|
+
return DEFAULT_NAMESPACES;
|
|
659
|
+
}
|
|
660
|
+
if (raw === "all") {
|
|
661
|
+
const result = await this.client.query(
|
|
662
|
+
_PostgresIntrospector.ALL_NAMESPACES_SQL
|
|
663
|
+
);
|
|
664
|
+
return result.rows.map((row) => row.nspname);
|
|
665
|
+
}
|
|
666
|
+
return raw;
|
|
667
|
+
}
|
|
668
|
+
static ALL_NAMESPACES_SQL = `
|
|
669
|
+
SELECT n.nspname
|
|
670
|
+
FROM pg_namespace n
|
|
671
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
672
|
+
AND n.nspname NOT LIKE 'pg_toast%'
|
|
673
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
674
|
+
ORDER BY n.nspname
|
|
675
|
+
`;
|
|
676
|
+
static applyEntityFilters(tables, options) {
|
|
677
|
+
const { includeEntities: allow, excludeEntities: deny } = options;
|
|
678
|
+
if (!allow && !deny) {
|
|
679
|
+
return tables;
|
|
680
|
+
}
|
|
681
|
+
return tables.filter((t) => {
|
|
682
|
+
if (allow && !allow.includes(t.name)) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
if (deny?.includes(t.name)) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/adapters/postgres/query/SqlIdentifier.ts
|
|
694
|
+
var SqlIdentifier = class _SqlIdentifier {
|
|
695
|
+
static quote(id) {
|
|
696
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
697
|
+
}
|
|
698
|
+
static qualified(namespace, name) {
|
|
699
|
+
return `${_SqlIdentifier.quote(namespace)}.${_SqlIdentifier.quote(name)}`;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// src/adapters/postgres/query/FilterBuilder.ts
|
|
704
|
+
var FilterBuilder = class {
|
|
705
|
+
constructor(params) {
|
|
706
|
+
this.params = params;
|
|
707
|
+
}
|
|
708
|
+
params;
|
|
709
|
+
build(filter) {
|
|
710
|
+
const col = SqlIdentifier.quote(filter.field);
|
|
711
|
+
switch (filter.operator) {
|
|
712
|
+
case "eq": {
|
|
713
|
+
return `${col} = ${this.params.add(filter.value)}`;
|
|
714
|
+
}
|
|
715
|
+
case "neq": {
|
|
716
|
+
return `${col} <> ${this.params.add(filter.value)}`;
|
|
717
|
+
}
|
|
718
|
+
case "lt": {
|
|
719
|
+
return `${col} < ${this.params.add(filter.value)}`;
|
|
720
|
+
}
|
|
721
|
+
case "lte": {
|
|
722
|
+
return `${col} <= ${this.params.add(filter.value)}`;
|
|
723
|
+
}
|
|
724
|
+
case "gt": {
|
|
725
|
+
return `${col} > ${this.params.add(filter.value)}`;
|
|
726
|
+
}
|
|
727
|
+
case "gte": {
|
|
728
|
+
return `${col} >= ${this.params.add(filter.value)}`;
|
|
729
|
+
}
|
|
730
|
+
case "like": {
|
|
731
|
+
return `${col} LIKE ${this.params.add(filter.value)}`;
|
|
732
|
+
}
|
|
733
|
+
case "ilike": {
|
|
734
|
+
return `${col} ILIKE ${this.params.add(filter.value)}`;
|
|
735
|
+
}
|
|
736
|
+
case "is-null": {
|
|
737
|
+
return `${col} IS NULL`;
|
|
738
|
+
}
|
|
739
|
+
case "is-not-null": {
|
|
740
|
+
return `${col} IS NOT NULL`;
|
|
741
|
+
}
|
|
742
|
+
case "in": {
|
|
743
|
+
return this.buildInClause(col, filter.value, "FALSE", "IN");
|
|
744
|
+
}
|
|
745
|
+
case "not-in": {
|
|
746
|
+
return this.buildInClause(col, filter.value, "TRUE", "NOT IN");
|
|
747
|
+
}
|
|
748
|
+
case "between": {
|
|
749
|
+
return this.buildBetween(col, filter.value);
|
|
750
|
+
}
|
|
751
|
+
default: {
|
|
752
|
+
const exhaustive = filter.operator;
|
|
753
|
+
throw new Error(`Unknown filter operator: ${String(exhaustive)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
buildInClause(col, value, emptyFallback, keyword) {
|
|
758
|
+
if (!Array.isArray(value)) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`Operator '${keyword.toLowerCase()}' requires an array value.`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
const values = value;
|
|
764
|
+
if (values.length === 0) {
|
|
765
|
+
return emptyFallback;
|
|
766
|
+
}
|
|
767
|
+
const placeholders = this.params.addAll(values);
|
|
768
|
+
return `${col} ${keyword} (${placeholders.join(", ")})`;
|
|
769
|
+
}
|
|
770
|
+
buildBetween(col, value) {
|
|
771
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
772
|
+
throw new Error("Operator 'between' requires a [min, max] tuple.");
|
|
773
|
+
}
|
|
774
|
+
const tuple = value;
|
|
775
|
+
const lo = this.params.add(tuple[0]);
|
|
776
|
+
const hi = this.params.add(tuple[1]);
|
|
777
|
+
return `${col} BETWEEN ${lo} AND ${hi}`;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// src/adapters/postgres/query/SqlParamList.ts
|
|
782
|
+
var SqlParamList = class {
|
|
783
|
+
values = [];
|
|
784
|
+
/**
|
|
785
|
+
* Append a value and return its placeholder (`$N`, where N is the
|
|
786
|
+
* 1-based position of the value in the list).
|
|
787
|
+
*/
|
|
788
|
+
add(value) {
|
|
789
|
+
this.values.push(value);
|
|
790
|
+
return `$${this.values.length}`;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Append many values and return their placeholders in order.
|
|
794
|
+
*/
|
|
795
|
+
addAll(values) {
|
|
796
|
+
return values.map((v) => this.add(v));
|
|
797
|
+
}
|
|
798
|
+
/** Number of values currently bound. */
|
|
799
|
+
get size() {
|
|
800
|
+
return this.values.length;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Return a read-only copy of the bound values. The returned array
|
|
804
|
+
* is not backed by the internal storage.
|
|
805
|
+
*/
|
|
806
|
+
toArray() {
|
|
807
|
+
return [...this.values];
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/adapters/postgres/PostgresQueryEngine.ts
|
|
812
|
+
var PostgresQueryEngine = class {
|
|
813
|
+
name = POSTGRES_ADAPTER_NAME;
|
|
814
|
+
kind = "relational";
|
|
815
|
+
build(spec, model) {
|
|
816
|
+
const entity = model.getEntity(spec.namespace, spec.entity);
|
|
817
|
+
if (!entity) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`Entity "${spec.namespace}.${spec.entity}" not found in the data model.`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
const fieldNames = new Set(entity.fields.map((f) => f.name));
|
|
823
|
+
const selectedFields = spec.select && spec.select.length > 0 ? [...spec.select] : entity.fields.map((f) => f.name);
|
|
824
|
+
for (const field of selectedFields) {
|
|
825
|
+
if (!fieldNames.has(field)) {
|
|
826
|
+
throw new Error(
|
|
827
|
+
`Field "${field}" does not exist on "${spec.namespace}.${spec.entity}".`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const params = new SqlParamList();
|
|
832
|
+
const filterBuilder = new FilterBuilder(params);
|
|
833
|
+
const selectClause = selectedFields.map(SqlIdentifier.quote).join(", ");
|
|
834
|
+
const fromClause = SqlIdentifier.qualified(spec.namespace, spec.entity);
|
|
835
|
+
let sql = `SELECT ${selectClause} FROM ${fromClause}`;
|
|
836
|
+
if (spec.filters && spec.filters.length > 0) {
|
|
837
|
+
const wherePieces = spec.filters.map((filter) => {
|
|
838
|
+
if (!fieldNames.has(filter.field)) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`Filter field "${filter.field}" does not exist on "${spec.namespace}.${spec.entity}".`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return filterBuilder.build(filter);
|
|
844
|
+
});
|
|
845
|
+
sql += ` WHERE ${wherePieces.join(" AND ")}`;
|
|
846
|
+
}
|
|
847
|
+
if (spec.orderBy && spec.orderBy.length > 0) {
|
|
848
|
+
const orderPieces = spec.orderBy.map((order) => {
|
|
849
|
+
if (!fieldNames.has(order.field)) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Order field "${order.field}" does not exist on "${spec.namespace}.${spec.entity}".`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return `${SqlIdentifier.quote(order.field)} ${order.direction.toUpperCase()}`;
|
|
855
|
+
});
|
|
856
|
+
sql += ` ORDER BY ${orderPieces.join(", ")}`;
|
|
857
|
+
}
|
|
858
|
+
if (spec.limit !== void 0) {
|
|
859
|
+
sql += ` LIMIT ${params.add(spec.limit)}`;
|
|
860
|
+
}
|
|
861
|
+
if (spec.offset !== void 0) {
|
|
862
|
+
sql += ` OFFSET ${params.add(spec.offset)}`;
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
command: sql,
|
|
866
|
+
params: params.toArray(),
|
|
867
|
+
metadata: {
|
|
868
|
+
engine: POSTGRES_ADAPTER_NAME,
|
|
869
|
+
paramCount: params.size
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/parsing/DefaultRecordParser.ts
|
|
876
|
+
var DefaultRecordParser = class {
|
|
877
|
+
parse(entity, row) {
|
|
878
|
+
const result = {};
|
|
879
|
+
for (const field of entity.fields) {
|
|
880
|
+
if (field.name in row) {
|
|
881
|
+
result[field.name] = this.coerce(field, row[field.name]);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
parseMany(entity, rows) {
|
|
887
|
+
return rows.map((row) => this.parse(entity, row));
|
|
888
|
+
}
|
|
889
|
+
coerce(field, raw) {
|
|
890
|
+
if (raw === null || raw === void 0) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
switch (field.type.category) {
|
|
894
|
+
case "string":
|
|
895
|
+
case "uuid":
|
|
896
|
+
case "enum": {
|
|
897
|
+
return String(raw);
|
|
898
|
+
}
|
|
899
|
+
case "integer": {
|
|
900
|
+
if (typeof raw === "bigint") {
|
|
901
|
+
return raw;
|
|
902
|
+
}
|
|
903
|
+
if (typeof raw === "number") {
|
|
904
|
+
return raw;
|
|
905
|
+
}
|
|
906
|
+
return Number(raw);
|
|
907
|
+
}
|
|
908
|
+
case "decimal": {
|
|
909
|
+
return typeof raw === "string" ? raw : String(raw);
|
|
910
|
+
}
|
|
911
|
+
case "boolean": {
|
|
912
|
+
return Boolean(raw);
|
|
913
|
+
}
|
|
914
|
+
case "date":
|
|
915
|
+
case "timestamp":
|
|
916
|
+
case "time": {
|
|
917
|
+
if (raw instanceof Date) {
|
|
918
|
+
return raw;
|
|
919
|
+
}
|
|
920
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
921
|
+
return new Date(raw);
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
case "json":
|
|
926
|
+
case "binary":
|
|
927
|
+
case "reference":
|
|
928
|
+
case "unknown": {
|
|
929
|
+
return raw;
|
|
930
|
+
}
|
|
931
|
+
case "array": {
|
|
932
|
+
return Array.isArray(raw) ? raw : [];
|
|
933
|
+
}
|
|
934
|
+
default: {
|
|
935
|
+
const exhaustive = field.type.category;
|
|
936
|
+
return exhaustive;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/adapters/postgres/PostgresRecordParser.ts
|
|
943
|
+
var BIGINT_NATIVE_TYPES = /* @__PURE__ */ new Set(["int8", "bigint"]);
|
|
944
|
+
var PostgresRecordParser = class extends DefaultRecordParser {
|
|
945
|
+
coerce(field, raw) {
|
|
946
|
+
if (raw === null || raw === void 0) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
if (field.type.category === "integer" && BIGINT_NATIVE_TYPES.has(field.type.nativeType.toLowerCase())) {
|
|
950
|
+
if (typeof raw === "bigint") {
|
|
951
|
+
return raw;
|
|
952
|
+
}
|
|
953
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
954
|
+
return BigInt(raw);
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
if (field.type.category === "json" && typeof raw === "string") {
|
|
959
|
+
try {
|
|
960
|
+
return JSON.parse(raw);
|
|
961
|
+
} catch {
|
|
962
|
+
return raw;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return super.coerce(field, raw);
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// src/adapters/postgres/PostgresRawQueryRunner.ts
|
|
970
|
+
var PostgresRawQueryRunner = class {
|
|
971
|
+
constructor(client) {
|
|
972
|
+
this.client = client;
|
|
973
|
+
}
|
|
974
|
+
client;
|
|
975
|
+
async run(built) {
|
|
976
|
+
const result = await this.client.query(built.command, [
|
|
977
|
+
...built.params
|
|
978
|
+
]);
|
|
979
|
+
return result.rows;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/adapters/postgres/postgresAdapter.ts
|
|
984
|
+
var postgresAdapter = {
|
|
985
|
+
name: POSTGRES_ADAPTER_NAME,
|
|
986
|
+
urlSchemes: POSTGRES_URL_SCHEMES,
|
|
987
|
+
create(client) {
|
|
988
|
+
return {
|
|
989
|
+
name: POSTGRES_ADAPTER_NAME,
|
|
990
|
+
introspector: new PostgresIntrospector(client),
|
|
991
|
+
engine: new PostgresQueryEngine(),
|
|
992
|
+
runner: new PostgresRawQueryRunner(client),
|
|
993
|
+
parser: new PostgresRecordParser(),
|
|
994
|
+
urlSchemes: POSTGRES_URL_SCHEMES
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
function isPostgresAdapter(adapter) {
|
|
999
|
+
return adapter.name === POSTGRES_ADAPTER_NAME;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/codegen/OverridesScaffold.ts
|
|
1003
|
+
var SCAFFOLD = `// biref.schema.overrides.ts - edit freely, regen will NOT touch this file.
|
|
1004
|
+
//
|
|
1005
|
+
// Use this file to attach concrete TypeScript types to columns whose
|
|
1006
|
+
// shape the database scanner cannot see - typically jsonb payloads.
|
|
1007
|
+
// Key each override by qualified entity name ('namespace.entity')
|
|
1008
|
+
// and list the fields you want to type.
|
|
1009
|
+
//
|
|
1010
|
+
// @example
|
|
1011
|
+
// export interface Overrides {
|
|
1012
|
+
// 'identity.users': {
|
|
1013
|
+
// profile: { plan: 'free' | 'pro'; prefs: { darkMode: boolean } };
|
|
1014
|
+
// };
|
|
1015
|
+
// }
|
|
1016
|
+
|
|
1017
|
+
export interface Overrides {}
|
|
1018
|
+
`;
|
|
1019
|
+
function overridesScaffold() {
|
|
1020
|
+
return SCAFFOLD;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/codegen/TsTypeMapper.ts
|
|
1024
|
+
function tsTypeFor(type, nullable) {
|
|
1025
|
+
const base = baseType(type);
|
|
1026
|
+
return nullable ? `${base} | null` : base;
|
|
1027
|
+
}
|
|
1028
|
+
function baseType(type) {
|
|
1029
|
+
switch (type.category) {
|
|
1030
|
+
case "string":
|
|
1031
|
+
case "uuid": {
|
|
1032
|
+
return "string";
|
|
1033
|
+
}
|
|
1034
|
+
case "enum": {
|
|
1035
|
+
if (type.enumValues && type.enumValues.length > 0) {
|
|
1036
|
+
return type.enumValues.map((value) => `'${escapeSingleQuote(value)}'`).join(" | ");
|
|
1037
|
+
}
|
|
1038
|
+
return "string";
|
|
1039
|
+
}
|
|
1040
|
+
case "integer": {
|
|
1041
|
+
return "number";
|
|
1042
|
+
}
|
|
1043
|
+
case "decimal": {
|
|
1044
|
+
return "string";
|
|
1045
|
+
}
|
|
1046
|
+
case "boolean": {
|
|
1047
|
+
return "boolean";
|
|
1048
|
+
}
|
|
1049
|
+
case "date":
|
|
1050
|
+
case "timestamp":
|
|
1051
|
+
case "time": {
|
|
1052
|
+
return "Date";
|
|
1053
|
+
}
|
|
1054
|
+
case "json": {
|
|
1055
|
+
return "unknown";
|
|
1056
|
+
}
|
|
1057
|
+
case "binary": {
|
|
1058
|
+
return "Uint8Array";
|
|
1059
|
+
}
|
|
1060
|
+
case "array": {
|
|
1061
|
+
const element = type.elementType ? baseType(type.elementType) : "unknown";
|
|
1062
|
+
return `ReadonlyArray<${element}>`;
|
|
1063
|
+
}
|
|
1064
|
+
case "reference":
|
|
1065
|
+
case "unknown": {
|
|
1066
|
+
return "unknown";
|
|
1067
|
+
}
|
|
1068
|
+
default: {
|
|
1069
|
+
return assertNever(type.category);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function escapeSingleQuote(value) {
|
|
1074
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1075
|
+
}
|
|
1076
|
+
function assertNever(value) {
|
|
1077
|
+
throw new Error(`Unhandled FieldTypeCategory: ${String(value)}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/query/relationNaming.ts
|
|
1081
|
+
function buildRelationNameMap(entity) {
|
|
1082
|
+
const result = /* @__PURE__ */ new Map();
|
|
1083
|
+
const used = /* @__PURE__ */ new Set();
|
|
1084
|
+
const proposals = [];
|
|
1085
|
+
entity.relationships.forEach((rel, order) => {
|
|
1086
|
+
const other = rel.direction === "outbound" ? rel.reference.toEntity : rel.reference.fromEntity;
|
|
1087
|
+
const isSelfRef = other.namespace === entity.namespace && other.name === entity.name;
|
|
1088
|
+
let name;
|
|
1089
|
+
if (isSelfRef) {
|
|
1090
|
+
name = rel.direction === "outbound" ? "parent" : "children";
|
|
1091
|
+
} else if (rel.direction === "outbound") {
|
|
1092
|
+
name = nameFromColumns(rel.reference.fromFields) ?? other.name;
|
|
1093
|
+
} else {
|
|
1094
|
+
name = other.name;
|
|
1095
|
+
}
|
|
1096
|
+
proposals.push({ rel, name, order });
|
|
1097
|
+
});
|
|
1098
|
+
proposals.sort((a, b) => {
|
|
1099
|
+
if (a.rel.direction !== b.rel.direction) {
|
|
1100
|
+
return a.rel.direction === "outbound" ? -1 : 1;
|
|
1101
|
+
}
|
|
1102
|
+
return a.order - b.order;
|
|
1103
|
+
});
|
|
1104
|
+
for (const { rel, name } of proposals) {
|
|
1105
|
+
let final = name;
|
|
1106
|
+
if (used.has(final)) {
|
|
1107
|
+
final = disambiguate(rel, name, used);
|
|
1108
|
+
}
|
|
1109
|
+
used.add(final);
|
|
1110
|
+
result.set(final, rel);
|
|
1111
|
+
}
|
|
1112
|
+
return result;
|
|
1113
|
+
}
|
|
1114
|
+
function nameFromColumns(columns) {
|
|
1115
|
+
if (columns.length === 0) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
const pieces = columns.map(stripIdSuffix).map((piece) => piece.trim()).filter((piece) => piece.length > 0 && piece.toLowerCase() !== "id");
|
|
1119
|
+
if (pieces.length === 0) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
return pieces.join("_");
|
|
1123
|
+
}
|
|
1124
|
+
function stripIdSuffix(column) {
|
|
1125
|
+
const underscore = column.match(/^(.*)_id$/i);
|
|
1126
|
+
if (underscore?.[1] !== void 0) {
|
|
1127
|
+
return underscore[1];
|
|
1128
|
+
}
|
|
1129
|
+
const camel = column.match(/^(.*)Id$/);
|
|
1130
|
+
if (camel?.[1] !== void 0) {
|
|
1131
|
+
return camel[1];
|
|
1132
|
+
}
|
|
1133
|
+
return column;
|
|
1134
|
+
}
|
|
1135
|
+
function disambiguate(rel, base, used) {
|
|
1136
|
+
if (rel.direction === "inbound") {
|
|
1137
|
+
const suffix = nameFromColumns(rel.reference.fromFields);
|
|
1138
|
+
if (suffix) {
|
|
1139
|
+
const candidate2 = `${base}_by_${suffix}`;
|
|
1140
|
+
if (!used.has(candidate2)) {
|
|
1141
|
+
return candidate2;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
const targetName = rel.direction === "outbound" ? rel.reference.toEntity.name : null;
|
|
1146
|
+
if (targetName) {
|
|
1147
|
+
const candidate2 = `${base}_${targetName}`;
|
|
1148
|
+
if (!used.has(candidate2)) {
|
|
1149
|
+
return candidate2;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
let candidate = rel.reference.name;
|
|
1154
|
+
if (!used.has(candidate)) {
|
|
1155
|
+
return candidate;
|
|
1156
|
+
}
|
|
1157
|
+
let counter = 2;
|
|
1158
|
+
while (used.has(candidate)) {
|
|
1159
|
+
candidate = `${rel.reference.name}_${counter}`;
|
|
1160
|
+
counter += 1;
|
|
1161
|
+
}
|
|
1162
|
+
return candidate;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/codegen/SchemaEmitter.ts
|
|
1166
|
+
var GENERATED_BANNER = `// This file is generated by @biref/scanner. Do not edit by hand.
|
|
1167
|
+
// Re-run \`biref gen\` to regenerate it from a live database scan.
|
|
1168
|
+
//
|
|
1169
|
+
// Overrides live in the sibling \`biref.schema.overrides.ts\` file
|
|
1170
|
+
// (never regenerated) and are deep-merged into \`BirefSchema\` via
|
|
1171
|
+
// \`ApplySchemaOverrides\`.`;
|
|
1172
|
+
function generateSchema(model) {
|
|
1173
|
+
const namespaces = groupByNamespace(model);
|
|
1174
|
+
const sortedNamespaces = [...namespaces.keys()].sort();
|
|
1175
|
+
const lines = [];
|
|
1176
|
+
lines.push(GENERATED_BANNER);
|
|
1177
|
+
lines.push("");
|
|
1178
|
+
lines.push("import type { ApplySchemaOverrides } from '@biref/scanner';");
|
|
1179
|
+
lines.push("import type { Overrides } from './biref.schema.overrides';");
|
|
1180
|
+
lines.push("");
|
|
1181
|
+
lines.push("export interface RawBirefSchema {");
|
|
1182
|
+
for (const namespace of sortedNamespaces) {
|
|
1183
|
+
const entities = namespaces.get(namespace);
|
|
1184
|
+
if (!entities) {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
lines.push(` readonly ${quoteKey(namespace)}: {`);
|
|
1188
|
+
const sortedEntities = [...entities].sort(
|
|
1189
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1190
|
+
);
|
|
1191
|
+
for (const entity of sortedEntities) {
|
|
1192
|
+
lines.push(` readonly ${quoteKey(entity.name)}: {`);
|
|
1193
|
+
lines.push(" readonly fields: {");
|
|
1194
|
+
for (const field of entity.fields) {
|
|
1195
|
+
lines.push(indent(formatFieldDescriptor(field), 4));
|
|
1196
|
+
}
|
|
1197
|
+
lines.push(" };");
|
|
1198
|
+
lines.push(` readonly identifier: ${formatIdentifier(entity)};`);
|
|
1199
|
+
const relations = sortedRelations(entity);
|
|
1200
|
+
if (relations.length === 0) {
|
|
1201
|
+
lines.push(" readonly relations: Record<string, never>;");
|
|
1202
|
+
} else {
|
|
1203
|
+
lines.push(" readonly relations: {");
|
|
1204
|
+
for (const [name, relationship] of relations) {
|
|
1205
|
+
lines.push(indent(formatRelationDescriptor(name, relationship), 4));
|
|
1206
|
+
}
|
|
1207
|
+
lines.push(" };");
|
|
1208
|
+
}
|
|
1209
|
+
lines.push(" };");
|
|
1210
|
+
}
|
|
1211
|
+
lines.push(" };");
|
|
1212
|
+
}
|
|
1213
|
+
lines.push("}");
|
|
1214
|
+
lines.push("");
|
|
1215
|
+
lines.push(
|
|
1216
|
+
"export type BirefSchema = ApplySchemaOverrides<RawBirefSchema, Overrides>;"
|
|
1217
|
+
);
|
|
1218
|
+
lines.push("");
|
|
1219
|
+
return lines.join("\n");
|
|
1220
|
+
}
|
|
1221
|
+
function generateSchemaFiles(model) {
|
|
1222
|
+
const namespaces = groupByNamespace(model);
|
|
1223
|
+
const sortedNamespaces = [...namespaces.keys()].sort();
|
|
1224
|
+
const files = [];
|
|
1225
|
+
for (const namespace of sortedNamespaces) {
|
|
1226
|
+
const entities = namespaces.get(namespace);
|
|
1227
|
+
if (!entities) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const sortedEntities = [...entities].sort(
|
|
1231
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1232
|
+
);
|
|
1233
|
+
for (const entity of sortedEntities) {
|
|
1234
|
+
files.push({
|
|
1235
|
+
path: `${safeSegment(namespace)}/${safeSegment(entity.name)}.ts`,
|
|
1236
|
+
content: emitEntityFile(entity)
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
files.push({
|
|
1241
|
+
path: "index.ts",
|
|
1242
|
+
content: emitIndexFile(model)
|
|
1243
|
+
});
|
|
1244
|
+
return files;
|
|
1245
|
+
}
|
|
1246
|
+
function emitEntityFile(entity) {
|
|
1247
|
+
const interfaceName = entityInterfaceName(entity.namespace, entity.name);
|
|
1248
|
+
const lines = [];
|
|
1249
|
+
lines.push(GENERATED_BANNER);
|
|
1250
|
+
lines.push("");
|
|
1251
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
1252
|
+
lines.push(" readonly fields: {");
|
|
1253
|
+
for (const field of entity.fields) {
|
|
1254
|
+
lines.push(indent(formatFieldDescriptor(field), 2));
|
|
1255
|
+
}
|
|
1256
|
+
lines.push(" };");
|
|
1257
|
+
lines.push(` readonly identifier: ${formatIdentifier(entity)};`);
|
|
1258
|
+
const relations = sortedRelations(entity);
|
|
1259
|
+
if (relations.length === 0) {
|
|
1260
|
+
lines.push(" readonly relations: Record<string, never>;");
|
|
1261
|
+
} else {
|
|
1262
|
+
lines.push(" readonly relations: {");
|
|
1263
|
+
for (const [name, relationship] of relations) {
|
|
1264
|
+
lines.push(indent(formatRelationDescriptor(name, relationship), 2));
|
|
1265
|
+
}
|
|
1266
|
+
lines.push(" };");
|
|
1267
|
+
}
|
|
1268
|
+
lines.push("}");
|
|
1269
|
+
lines.push("");
|
|
1270
|
+
return lines.join("\n");
|
|
1271
|
+
}
|
|
1272
|
+
function emitIndexFile(model) {
|
|
1273
|
+
const namespaces = groupByNamespace(model);
|
|
1274
|
+
const sortedNamespaces = [...namespaces.keys()].sort();
|
|
1275
|
+
const lines = [];
|
|
1276
|
+
lines.push(GENERATED_BANNER);
|
|
1277
|
+
lines.push("");
|
|
1278
|
+
lines.push("import type { ApplySchemaOverrides } from '@biref/scanner';");
|
|
1279
|
+
lines.push("import type { Overrides } from './biref.schema.overrides';");
|
|
1280
|
+
lines.push("");
|
|
1281
|
+
const allEntities = [];
|
|
1282
|
+
for (const namespace of sortedNamespaces) {
|
|
1283
|
+
const entities = namespaces.get(namespace);
|
|
1284
|
+
if (!entities) {
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
for (const entity of [...entities].sort(
|
|
1288
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1289
|
+
)) {
|
|
1290
|
+
allEntities.push({ namespace, entity });
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
for (const { namespace, entity } of allEntities) {
|
|
1294
|
+
const iface = entityInterfaceName(namespace, entity.name);
|
|
1295
|
+
lines.push(
|
|
1296
|
+
`import type { ${iface} } from './${safeSegment(namespace)}/${safeSegment(entity.name)}';`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
lines.push("");
|
|
1300
|
+
for (const { namespace, entity } of allEntities) {
|
|
1301
|
+
const iface = entityInterfaceName(namespace, entity.name);
|
|
1302
|
+
lines.push(
|
|
1303
|
+
`export type { ${iface} } from './${safeSegment(namespace)}/${safeSegment(entity.name)}';`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
lines.push("export interface RawBirefSchema {");
|
|
1308
|
+
for (const namespace of sortedNamespaces) {
|
|
1309
|
+
const entities = namespaces.get(namespace);
|
|
1310
|
+
if (!entities) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
lines.push(` readonly ${quoteKey(namespace)}: {`);
|
|
1314
|
+
const sortedEntities = [...entities].sort(
|
|
1315
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1316
|
+
);
|
|
1317
|
+
for (const entity of sortedEntities) {
|
|
1318
|
+
const iface = entityInterfaceName(namespace, entity.name);
|
|
1319
|
+
lines.push(` readonly ${quoteKey(entity.name)}: ${iface};`);
|
|
1320
|
+
}
|
|
1321
|
+
lines.push(" };");
|
|
1322
|
+
}
|
|
1323
|
+
lines.push("}");
|
|
1324
|
+
lines.push("");
|
|
1325
|
+
lines.push(
|
|
1326
|
+
"export type BirefSchema = ApplySchemaOverrides<RawBirefSchema, Overrides>;"
|
|
1327
|
+
);
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
return lines.join("\n");
|
|
1330
|
+
}
|
|
1331
|
+
function groupByNamespace(model) {
|
|
1332
|
+
const map = /* @__PURE__ */ new Map();
|
|
1333
|
+
for (const entity of model.entities) {
|
|
1334
|
+
const bucket = map.get(entity.namespace);
|
|
1335
|
+
if (bucket) {
|
|
1336
|
+
bucket.push(entity);
|
|
1337
|
+
} else {
|
|
1338
|
+
map.set(entity.namespace, [entity]);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return map;
|
|
1342
|
+
}
|
|
1343
|
+
function sortedRelations(entity) {
|
|
1344
|
+
return [...buildRelationNameMap(entity).entries()].sort(
|
|
1345
|
+
([a], [b]) => a.localeCompare(b)
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
function formatFieldDescriptor(field) {
|
|
1349
|
+
const ts = tsTypeFor(field.type, field.nullable);
|
|
1350
|
+
const nullable = field.nullable ? "true" : "false";
|
|
1351
|
+
const isIdentifier = field.isIdentifier ? "true" : "false";
|
|
1352
|
+
const category = `'${field.type.category}'`;
|
|
1353
|
+
return ` readonly ${quoteKey(field.name)}: { readonly ts: ${ts}; readonly nullable: ${nullable}; readonly isIdentifier: ${isIdentifier}; readonly category: ${category} };`;
|
|
1354
|
+
}
|
|
1355
|
+
function formatIdentifier(entity) {
|
|
1356
|
+
if (entity.identifier.length === 0) {
|
|
1357
|
+
return "readonly []";
|
|
1358
|
+
}
|
|
1359
|
+
const items = entity.identifier.map((name) => `'${escapeString(name)}'`);
|
|
1360
|
+
return `readonly [${items.join(", ")}]`;
|
|
1361
|
+
}
|
|
1362
|
+
function formatRelationDescriptor(name, relationship) {
|
|
1363
|
+
const target = relationship.direction === "outbound" ? relationship.reference.toEntity : relationship.reference.fromEntity;
|
|
1364
|
+
const qualifiedTarget = `'${escapeString(target.namespace)}.${escapeString(target.name)}'`;
|
|
1365
|
+
const fromFields = formatStringArray(relationship.reference.fromFields);
|
|
1366
|
+
const toFields = formatStringArray(relationship.reference.toFields);
|
|
1367
|
+
const cardinality = relationship.direction === "outbound" ? "'one'" : "'many'";
|
|
1368
|
+
const direction = `'${relationship.direction}'`;
|
|
1369
|
+
return ` readonly ${quoteKey(name)}: { readonly direction: ${direction}; readonly target: ${qualifiedTarget}; readonly fromFields: ${fromFields}; readonly toFields: ${toFields}; readonly cardinality: ${cardinality} };`;
|
|
1370
|
+
}
|
|
1371
|
+
function formatStringArray(values) {
|
|
1372
|
+
if (values.length === 0) {
|
|
1373
|
+
return "readonly []";
|
|
1374
|
+
}
|
|
1375
|
+
return `readonly [${values.map((v) => `'${escapeString(v)}'`).join(", ")}]`;
|
|
1376
|
+
}
|
|
1377
|
+
function quoteKey(key) {
|
|
1378
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {
|
|
1379
|
+
return key;
|
|
1380
|
+
}
|
|
1381
|
+
return `'${escapeString(key)}'`;
|
|
1382
|
+
}
|
|
1383
|
+
function escapeString(value) {
|
|
1384
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1385
|
+
}
|
|
1386
|
+
function entityInterfaceName(namespace, entity) {
|
|
1387
|
+
return toPascalCase(namespace) + toPascalCase(entity);
|
|
1388
|
+
}
|
|
1389
|
+
function toPascalCase(value) {
|
|
1390
|
+
return value.split(/[^A-Za-z0-9]+/).filter((segment) => segment.length > 0).map(
|
|
1391
|
+
(segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()
|
|
1392
|
+
).join("");
|
|
1393
|
+
}
|
|
1394
|
+
function safeSegment(name) {
|
|
1395
|
+
return name.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1396
|
+
}
|
|
1397
|
+
function indent(text, spaces) {
|
|
1398
|
+
const pad = " ".repeat(spaces);
|
|
1399
|
+
return text.split("\n").map((line) => line.length === 0 ? line : `${pad}${line}`).join("\n");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/core/AdapterRegistry.ts
|
|
1403
|
+
var AdapterRegistry = class {
|
|
1404
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1405
|
+
register(adapter) {
|
|
1406
|
+
if (this.adapters.has(adapter.name)) {
|
|
1407
|
+
throw new Error(`Adapter "${adapter.name}" is already registered.`);
|
|
1408
|
+
}
|
|
1409
|
+
this.adapters.set(adapter.name, adapter);
|
|
1410
|
+
}
|
|
1411
|
+
unregister(name) {
|
|
1412
|
+
return this.adapters.delete(name);
|
|
1413
|
+
}
|
|
1414
|
+
get(name) {
|
|
1415
|
+
const adapter = this.adapters.get(name);
|
|
1416
|
+
if (!adapter) {
|
|
1417
|
+
const known = [...this.adapters.keys()].join(", ") || "(none)";
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`No adapter registered under "${name}". Registered adapters: ${known}.`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
return adapter;
|
|
1423
|
+
}
|
|
1424
|
+
has(name) {
|
|
1425
|
+
return this.adapters.has(name);
|
|
1426
|
+
}
|
|
1427
|
+
list() {
|
|
1428
|
+
return [...this.adapters.keys()];
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Find an adapter that claims the given URL scheme. Case-insensitive.
|
|
1432
|
+
* Returns `undefined` if no adapter handles it.
|
|
1433
|
+
*/
|
|
1434
|
+
findByUrlScheme(scheme) {
|
|
1435
|
+
const normalized = scheme.toLowerCase();
|
|
1436
|
+
for (const adapter of this.adapters.values()) {
|
|
1437
|
+
const matches = adapter.urlSchemes?.some(
|
|
1438
|
+
(s) => s.toLowerCase() === normalized
|
|
1439
|
+
);
|
|
1440
|
+
if (matches) {
|
|
1441
|
+
return adapter;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return void 0;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Same as `findByUrlScheme` but throws a helpful error listing the
|
|
1448
|
+
* known schemes when no match is found.
|
|
1449
|
+
*/
|
|
1450
|
+
getByUrlScheme(scheme) {
|
|
1451
|
+
const adapter = this.findByUrlScheme(scheme);
|
|
1452
|
+
if (!adapter) {
|
|
1453
|
+
const known = [...this.adapters.values()].flatMap((a) => a.urlSchemes ?? []).join(", ") || "(none)";
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
`No adapter registered for URL scheme "${scheme}". Known schemes: ${known}.`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
return adapter;
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
// src/core/QueryPlanExecutor.ts
|
|
1463
|
+
var QueryPlanExecutor = class _QueryPlanExecutor {
|
|
1464
|
+
constructor(engine, runner, parser) {
|
|
1465
|
+
this.engine = engine;
|
|
1466
|
+
this.runner = runner;
|
|
1467
|
+
this.parser = parser;
|
|
1468
|
+
}
|
|
1469
|
+
engine;
|
|
1470
|
+
runner;
|
|
1471
|
+
parser;
|
|
1472
|
+
async execute(plan, model) {
|
|
1473
|
+
const entity = model.getEntity(plan.spec.namespace, plan.spec.entity);
|
|
1474
|
+
if (!entity) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`Entity "${plan.spec.namespace}.${plan.spec.entity}" not found in the data model.`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
const exposedKeys = _QueryPlanExecutor.resolveExposedKeys(
|
|
1480
|
+
plan.spec,
|
|
1481
|
+
entity,
|
|
1482
|
+
plan.includes
|
|
1483
|
+
);
|
|
1484
|
+
const normalizedSpec = _QueryPlanExecutor.injectFields(
|
|
1485
|
+
plan.spec,
|
|
1486
|
+
_QueryPlanExecutor.collectParentKeys(plan.includes)
|
|
1487
|
+
);
|
|
1488
|
+
const built = this.engine.build(normalizedSpec, model);
|
|
1489
|
+
const rawRows = await this.runner.run(built);
|
|
1490
|
+
const parentRecords = this.parser.parseMany(entity, rawRows);
|
|
1491
|
+
if (plan.includes.length === 0) {
|
|
1492
|
+
return _QueryPlanExecutor.project(parentRecords, exposedKeys);
|
|
1493
|
+
}
|
|
1494
|
+
return this.attachIncludes(
|
|
1495
|
+
parentRecords,
|
|
1496
|
+
plan.includes,
|
|
1497
|
+
exposedKeys,
|
|
1498
|
+
model
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
async attachIncludes(parents, includes, parentUserSelect, model) {
|
|
1502
|
+
const working = parents.map((p) => ({
|
|
1503
|
+
...p
|
|
1504
|
+
}));
|
|
1505
|
+
for (const include of includes) {
|
|
1506
|
+
const childPlan = _QueryPlanExecutor.prepareChildPlan(include, working);
|
|
1507
|
+
if (childPlan === null) {
|
|
1508
|
+
for (const parent of working) {
|
|
1509
|
+
parent[include.relationName] = include.cardinality === "one" ? null : [];
|
|
1510
|
+
}
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const rawChildren = await this.execute(childPlan, model);
|
|
1514
|
+
const childExposedKeys = _QueryPlanExecutor.resolveChildExposedKeys(
|
|
1515
|
+
include.plan,
|
|
1516
|
+
model
|
|
1517
|
+
);
|
|
1518
|
+
const indexingKeys = new Set(childExposedKeys);
|
|
1519
|
+
for (const key of include.childKeys) {
|
|
1520
|
+
indexingKeys.add(key);
|
|
1521
|
+
}
|
|
1522
|
+
const byKey = _QueryPlanExecutor.indexByKey(
|
|
1523
|
+
rawChildren.map((c) => _QueryPlanExecutor.filterKeys(c, indexingKeys)),
|
|
1524
|
+
include.childKeys
|
|
1525
|
+
);
|
|
1526
|
+
for (const parent of working) {
|
|
1527
|
+
const lookupKey = _QueryPlanExecutor.buildKey(
|
|
1528
|
+
parent,
|
|
1529
|
+
include.parentKeys
|
|
1530
|
+
);
|
|
1531
|
+
const matches = lookupKey === null ? [] : byKey.get(lookupKey) ?? [];
|
|
1532
|
+
const projectedMatches = _QueryPlanExecutor.project(
|
|
1533
|
+
matches,
|
|
1534
|
+
childExposedKeys
|
|
1535
|
+
);
|
|
1536
|
+
parent[include.relationName] = include.cardinality === "one" ? projectedMatches[0] ?? null : [...projectedMatches];
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return working.map(
|
|
1540
|
+
(parent) => _QueryPlanExecutor.filterKeys(parent, parentUserSelect)
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
static prepareChildPlan(include, parents) {
|
|
1544
|
+
if (parents.length === 0) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (include.parentKeys.length === 0 || include.childKeys.length === 0) {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
const collected = include.parentKeys.map(() => []);
|
|
1551
|
+
let anyMatch = false;
|
|
1552
|
+
for (const parent of parents) {
|
|
1553
|
+
let hasAll = true;
|
|
1554
|
+
const tuple = [];
|
|
1555
|
+
for (const key of include.parentKeys) {
|
|
1556
|
+
const value = parent[key];
|
|
1557
|
+
if (value === null || value === void 0) {
|
|
1558
|
+
hasAll = false;
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
tuple.push(value);
|
|
1562
|
+
}
|
|
1563
|
+
if (!hasAll) {
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
for (let i = 0; i < tuple.length; i += 1) {
|
|
1567
|
+
const bucket = collected[i];
|
|
1568
|
+
if (bucket) {
|
|
1569
|
+
bucket.push(tuple[i]);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
anyMatch = true;
|
|
1573
|
+
}
|
|
1574
|
+
if (!anyMatch) {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
let plan = {
|
|
1578
|
+
...include.plan,
|
|
1579
|
+
spec: _QueryPlanExecutor.injectFields(
|
|
1580
|
+
include.plan.spec,
|
|
1581
|
+
include.childKeys
|
|
1582
|
+
)
|
|
1583
|
+
};
|
|
1584
|
+
for (let i = 0; i < include.childKeys.length; i += 1) {
|
|
1585
|
+
const childKey = include.childKeys[i];
|
|
1586
|
+
const bucket = collected[i];
|
|
1587
|
+
if (childKey === void 0 || bucket === void 0) {
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
const unique = Array.from(new Set(bucket));
|
|
1591
|
+
const filter = {
|
|
1592
|
+
field: childKey,
|
|
1593
|
+
operator: "in",
|
|
1594
|
+
value: unique
|
|
1595
|
+
};
|
|
1596
|
+
plan = {
|
|
1597
|
+
...plan,
|
|
1598
|
+
spec: {
|
|
1599
|
+
...plan.spec,
|
|
1600
|
+
filters: [...plan.spec.filters ?? [], filter]
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
return plan;
|
|
1605
|
+
}
|
|
1606
|
+
static injectFields(spec, fields) {
|
|
1607
|
+
if (!spec.select || spec.select.length === 0) {
|
|
1608
|
+
return spec;
|
|
1609
|
+
}
|
|
1610
|
+
if (fields.length === 0) {
|
|
1611
|
+
return spec;
|
|
1612
|
+
}
|
|
1613
|
+
const seen = new Set(spec.select);
|
|
1614
|
+
const extras = [];
|
|
1615
|
+
for (const field of fields) {
|
|
1616
|
+
if (!seen.has(field)) {
|
|
1617
|
+
seen.add(field);
|
|
1618
|
+
extras.push(field);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (extras.length === 0) {
|
|
1622
|
+
return spec;
|
|
1623
|
+
}
|
|
1624
|
+
return { ...spec, select: [...spec.select, ...extras] };
|
|
1625
|
+
}
|
|
1626
|
+
static collectParentKeys(includes) {
|
|
1627
|
+
const keys = [];
|
|
1628
|
+
for (const include of includes) {
|
|
1629
|
+
for (const key of include.parentKeys) {
|
|
1630
|
+
keys.push(key);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return keys;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Compute the set of keys a hydrated record should expose to the
|
|
1637
|
+
* caller: the user's selected fields (or every entity field when
|
|
1638
|
+
* no select was given) plus every nested include's relation name
|
|
1639
|
+
* so attached children survive the final filter step.
|
|
1640
|
+
*/
|
|
1641
|
+
static resolveExposedKeys(spec, entity, includes) {
|
|
1642
|
+
const set = /* @__PURE__ */ new Set();
|
|
1643
|
+
if (spec.select && spec.select.length > 0) {
|
|
1644
|
+
for (const field of spec.select) {
|
|
1645
|
+
set.add(field);
|
|
1646
|
+
}
|
|
1647
|
+
} else {
|
|
1648
|
+
for (const field of entity.fields) {
|
|
1649
|
+
set.add(field.name);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
for (const include of includes) {
|
|
1653
|
+
set.add(include.relationName);
|
|
1654
|
+
}
|
|
1655
|
+
return set;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Resolve the exposed-keys set for an include's nested plan.
|
|
1659
|
+
*
|
|
1660
|
+
* Uses the ORIGINAL include.plan (not the mutated child plan we
|
|
1661
|
+
* handed to the executor) so injected join keys do not leak into
|
|
1662
|
+
* the final output.
|
|
1663
|
+
*/
|
|
1664
|
+
static resolveChildExposedKeys(plan, model) {
|
|
1665
|
+
const entity = model.getEntity(plan.spec.namespace, plan.spec.entity);
|
|
1666
|
+
if (!entity) {
|
|
1667
|
+
return /* @__PURE__ */ new Set();
|
|
1668
|
+
}
|
|
1669
|
+
return _QueryPlanExecutor.resolveExposedKeys(
|
|
1670
|
+
plan.spec,
|
|
1671
|
+
entity,
|
|
1672
|
+
plan.includes
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
static project(records, allow) {
|
|
1676
|
+
return records.map((record) => _QueryPlanExecutor.filterKeys(record, allow));
|
|
1677
|
+
}
|
|
1678
|
+
static indexByKey(records, keys) {
|
|
1679
|
+
const map = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const record of records) {
|
|
1681
|
+
const key = _QueryPlanExecutor.buildKey(record, keys);
|
|
1682
|
+
if (key === null) {
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
const bucket = map.get(key);
|
|
1686
|
+
if (bucket) {
|
|
1687
|
+
bucket.push(record);
|
|
1688
|
+
} else {
|
|
1689
|
+
map.set(key, [record]);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return map;
|
|
1693
|
+
}
|
|
1694
|
+
static buildKey(record, keys) {
|
|
1695
|
+
if (keys.length === 0) {
|
|
1696
|
+
return "";
|
|
1697
|
+
}
|
|
1698
|
+
const parts = [];
|
|
1699
|
+
for (const key of keys) {
|
|
1700
|
+
const value = record[key];
|
|
1701
|
+
if (value === null || value === void 0) {
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
parts.push(
|
|
1705
|
+
typeof value === "bigint" ? `bigint:${value.toString()}` : value
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
return JSON.stringify(parts);
|
|
1709
|
+
}
|
|
1710
|
+
static filterKeys(record, allow) {
|
|
1711
|
+
const result = {};
|
|
1712
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1713
|
+
if (allow.has(key)) {
|
|
1714
|
+
result[key] = value;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return result;
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// src/query/ChainBuilder.ts
|
|
1722
|
+
var ChainBuilder = class _ChainBuilder {
|
|
1723
|
+
constructor(ctx, plan) {
|
|
1724
|
+
this.ctx = ctx;
|
|
1725
|
+
this.plan = plan;
|
|
1726
|
+
}
|
|
1727
|
+
ctx;
|
|
1728
|
+
plan;
|
|
1729
|
+
/**
|
|
1730
|
+
* Narrow the projection to a specific set of fields.
|
|
1731
|
+
*
|
|
1732
|
+
* .select('id', 'email') -- only these two columns
|
|
1733
|
+
* .select('*') -- every field of the entity
|
|
1734
|
+
* .select() -- equivalent to '*', kept for ergonomic parity
|
|
1735
|
+
*
|
|
1736
|
+
* Unknown field names throw immediately with a clear error; the
|
|
1737
|
+
* wildcard sentinel skips validation and leaves the plan's `select`
|
|
1738
|
+
* undefined so the engine emits every column the adapter reported.
|
|
1739
|
+
*/
|
|
1740
|
+
select(...fields) {
|
|
1741
|
+
if (fields.length === 0 || fields.some((f) => f === "*")) {
|
|
1742
|
+
if (fields.some((f) => f !== "*")) {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
"select('*') cannot be combined with other field names. Pass either '*' or the specific columns you want."
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
return new _ChainBuilder(this.ctx, {
|
|
1748
|
+
...this.plan,
|
|
1749
|
+
spec: { ...this.plan.spec, select: void 0 }
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
const entity = this.requireEntity();
|
|
1753
|
+
const known = new Set(entity.fields.map((f) => f.name));
|
|
1754
|
+
for (const field of fields) {
|
|
1755
|
+
if (!known.has(field)) {
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
`Field "${field}" does not exist on "${this.plan.spec.namespace}.${this.plan.spec.entity}".`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return new _ChainBuilder(this.ctx, {
|
|
1762
|
+
...this.plan,
|
|
1763
|
+
spec: { ...this.plan.spec, select: [...fields] }
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
where(field, operator, value) {
|
|
1767
|
+
const entity = this.requireEntity();
|
|
1768
|
+
if (!entity.fields.some((f) => f.name === field)) {
|
|
1769
|
+
throw new Error(
|
|
1770
|
+
`Filter field "${field}" does not exist on "${this.plan.spec.namespace}.${this.plan.spec.entity}".`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const filter = {
|
|
1774
|
+
field,
|
|
1775
|
+
operator,
|
|
1776
|
+
value: value ?? null
|
|
1777
|
+
};
|
|
1778
|
+
return new _ChainBuilder(this.ctx, {
|
|
1779
|
+
...this.plan,
|
|
1780
|
+
spec: {
|
|
1781
|
+
...this.plan.spec,
|
|
1782
|
+
filters: [...this.plan.spec.filters ?? [], filter]
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
orderBy(field, direction = "asc") {
|
|
1787
|
+
const entity = this.requireEntity();
|
|
1788
|
+
if (!entity.fields.some((f) => f.name === field)) {
|
|
1789
|
+
throw new Error(
|
|
1790
|
+
`Order field "${field}" does not exist on "${this.plan.spec.namespace}.${this.plan.spec.entity}".`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
const order = { field, direction };
|
|
1794
|
+
return new _ChainBuilder(this.ctx, {
|
|
1795
|
+
...this.plan,
|
|
1796
|
+
spec: {
|
|
1797
|
+
...this.plan.spec,
|
|
1798
|
+
orderBy: [...this.plan.spec.orderBy ?? [], order]
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
limit(count) {
|
|
1803
|
+
return new _ChainBuilder(this.ctx, {
|
|
1804
|
+
...this.plan,
|
|
1805
|
+
spec: { ...this.plan.spec, limit: count }
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
offset(count) {
|
|
1809
|
+
return new _ChainBuilder(this.ctx, {
|
|
1810
|
+
...this.plan,
|
|
1811
|
+
spec: { ...this.plan.spec, offset: count }
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Attach a nested include to the plan.
|
|
1816
|
+
*
|
|
1817
|
+
* Three call shapes are supported:
|
|
1818
|
+
*
|
|
1819
|
+
* .include('orders') -- all fields, no nested
|
|
1820
|
+
* .include('orders', (q) => q.select('id', 'total')) -- narrow the child
|
|
1821
|
+
* .include('*') -- every relation on this entity
|
|
1822
|
+
*
|
|
1823
|
+
* The callback is optional. When omitted, the child plan uses the
|
|
1824
|
+
* default projection (all fields of the related entity) and no
|
|
1825
|
+
* further includes. Pass a callback when you want to narrow the
|
|
1826
|
+
* projection or chain additional nested includes.
|
|
1827
|
+
*
|
|
1828
|
+
* The wildcard `'*'` expands to one include per relation discovered
|
|
1829
|
+
* on the current entity. Each expanded include uses the default
|
|
1830
|
+
* projection. Combining `'*'` with a callback is not supported -
|
|
1831
|
+
* the sub-builder's shape differs per relation, so a single
|
|
1832
|
+
* callback cannot narrow all of them coherently.
|
|
1833
|
+
*/
|
|
1834
|
+
include(relationName, build) {
|
|
1835
|
+
const entity = this.requireEntity();
|
|
1836
|
+
const relationMap = buildRelationNameMap(entity);
|
|
1837
|
+
if (relationName === "*") {
|
|
1838
|
+
if (build) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
"Wildcard include '*' does not accept a builder callback. Use named includes to narrow individual relations."
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
let current = this;
|
|
1844
|
+
for (const name of relationMap.keys()) {
|
|
1845
|
+
current = current.include(name);
|
|
1846
|
+
}
|
|
1847
|
+
return current;
|
|
1848
|
+
}
|
|
1849
|
+
const relationship = relationMap.get(relationName);
|
|
1850
|
+
if (!relationship) {
|
|
1851
|
+
const known = [...relationMap.keys()].join(", ") || "(none)";
|
|
1852
|
+
throw new Error(
|
|
1853
|
+
`Relation "${relationName}" does not exist on "${this.plan.spec.namespace}.${this.plan.spec.entity}". Known relations: ${known}.`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
const target = relationship.direction === "outbound" ? relationship.reference.toEntity : relationship.reference.fromEntity;
|
|
1857
|
+
const childSpec = {
|
|
1858
|
+
namespace: target.namespace,
|
|
1859
|
+
entity: target.name
|
|
1860
|
+
};
|
|
1861
|
+
const childBuilder = new _ChainBuilder(this.ctx, {
|
|
1862
|
+
spec: childSpec,
|
|
1863
|
+
includes: []
|
|
1864
|
+
});
|
|
1865
|
+
const finishedChild = build ? build(childBuilder) : childBuilder;
|
|
1866
|
+
const parentKeys = relationship.direction === "outbound" ? relationship.reference.fromFields : relationship.reference.toFields;
|
|
1867
|
+
const childKeys = relationship.direction === "outbound" ? relationship.reference.toFields : relationship.reference.fromFields;
|
|
1868
|
+
const include = {
|
|
1869
|
+
relationName,
|
|
1870
|
+
plan: finishedChild.plan,
|
|
1871
|
+
direction: relationship.direction,
|
|
1872
|
+
parentKeys,
|
|
1873
|
+
childKeys,
|
|
1874
|
+
cardinality: relationship.direction === "outbound" ? "one" : "many"
|
|
1875
|
+
};
|
|
1876
|
+
return new _ChainBuilder(this.ctx, {
|
|
1877
|
+
...this.plan,
|
|
1878
|
+
includes: [...this.plan.includes, include]
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
async findMany() {
|
|
1882
|
+
return this.ctx.executor.execute(this.plan, this.ctx.model);
|
|
1883
|
+
}
|
|
1884
|
+
async findFirst() {
|
|
1885
|
+
const limited = {
|
|
1886
|
+
...this.plan,
|
|
1887
|
+
spec: { ...this.plan.spec, limit: 1 }
|
|
1888
|
+
};
|
|
1889
|
+
const rows = await this.ctx.executor.execute(limited, this.ctx.model);
|
|
1890
|
+
return rows[0] ?? null;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Escape hatch for advanced callers (tests, tooling): exposes the
|
|
1894
|
+
* accumulated plan without executing it. The typed builder's public
|
|
1895
|
+
* surface does not reference this directly.
|
|
1896
|
+
*/
|
|
1897
|
+
toPlan() {
|
|
1898
|
+
return this.plan;
|
|
1899
|
+
}
|
|
1900
|
+
requireEntity() {
|
|
1901
|
+
const entity = this.ctx.model.getEntity(
|
|
1902
|
+
this.plan.spec.namespace,
|
|
1903
|
+
this.plan.spec.entity
|
|
1904
|
+
);
|
|
1905
|
+
if (!entity) {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`Entity "${this.plan.spec.namespace}.${this.plan.spec.entity}" not found in the data model.`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return entity;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
// src/query/NamespaceProxy.ts
|
|
1915
|
+
function createNamespaceProxy(ctx) {
|
|
1916
|
+
const namespaces = collectNamespaces(ctx.model);
|
|
1917
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
1918
|
+
get(_target, prop) {
|
|
1919
|
+
if (typeof prop !== "string") {
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1922
|
+
if (!namespaces.has(prop)) {
|
|
1923
|
+
return void 0;
|
|
1924
|
+
}
|
|
1925
|
+
return createEntityProxy(ctx, prop);
|
|
1926
|
+
},
|
|
1927
|
+
has(_target, prop) {
|
|
1928
|
+
return typeof prop === "string" && namespaces.has(prop);
|
|
1929
|
+
},
|
|
1930
|
+
ownKeys() {
|
|
1931
|
+
return [...namespaces];
|
|
1932
|
+
},
|
|
1933
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
1934
|
+
if (typeof prop !== "string" || !namespaces.has(prop)) {
|
|
1935
|
+
return void 0;
|
|
1936
|
+
}
|
|
1937
|
+
return {
|
|
1938
|
+
enumerable: true,
|
|
1939
|
+
configurable: true,
|
|
1940
|
+
value: createEntityProxy(ctx, prop)
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
function createEntityProxy(ctx, namespace) {
|
|
1946
|
+
const entities = collectEntities(ctx.model, namespace);
|
|
1947
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
1948
|
+
get(_target, prop) {
|
|
1949
|
+
if (typeof prop !== "string") {
|
|
1950
|
+
return void 0;
|
|
1951
|
+
}
|
|
1952
|
+
if (!entities.has(prop)) {
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
return new ChainBuilder(ctx, {
|
|
1956
|
+
spec: { namespace, entity: prop },
|
|
1957
|
+
includes: []
|
|
1958
|
+
});
|
|
1959
|
+
},
|
|
1960
|
+
has(_target, prop) {
|
|
1961
|
+
return typeof prop === "string" && entities.has(prop);
|
|
1962
|
+
},
|
|
1963
|
+
ownKeys() {
|
|
1964
|
+
return [...entities];
|
|
1965
|
+
},
|
|
1966
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
1967
|
+
if (typeof prop !== "string" || !entities.has(prop)) {
|
|
1968
|
+
return void 0;
|
|
1969
|
+
}
|
|
1970
|
+
return {
|
|
1971
|
+
enumerable: true,
|
|
1972
|
+
configurable: true,
|
|
1973
|
+
value: new ChainBuilder(ctx, {
|
|
1974
|
+
spec: { namespace, entity: prop },
|
|
1975
|
+
includes: []
|
|
1976
|
+
})
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
function collectNamespaces(model) {
|
|
1982
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
1983
|
+
for (const entity of model.entities) {
|
|
1984
|
+
namespaces.add(entity.namespace);
|
|
1985
|
+
}
|
|
1986
|
+
return namespaces;
|
|
1987
|
+
}
|
|
1988
|
+
function collectEntities(model, namespace) {
|
|
1989
|
+
const entities = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const entity of model.entities) {
|
|
1991
|
+
if (entity.namespace === namespace) {
|
|
1992
|
+
entities.add(entity.name);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return entities;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/core/Biref.ts
|
|
1999
|
+
var Biref = class {
|
|
2000
|
+
constructor(registry) {
|
|
2001
|
+
this.registry = registry;
|
|
2002
|
+
}
|
|
2003
|
+
registry;
|
|
2004
|
+
/** Entry point for the fluent builder. */
|
|
2005
|
+
static builder() {
|
|
2006
|
+
return new BirefBuilder();
|
|
2007
|
+
}
|
|
2008
|
+
async scan(adapterNameOrOptions, maybeOptions) {
|
|
2009
|
+
const { adapter, options } = this.resolveScanArgs(
|
|
2010
|
+
adapterNameOrOptions,
|
|
2011
|
+
maybeOptions
|
|
2012
|
+
);
|
|
2013
|
+
return adapter.introspector.introspect(options);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Introspect a data store using whichever registered adapter handles
|
|
2017
|
+
* the URL's scheme. The URL is only used to pick the adapter; the
|
|
2018
|
+
* actual connection is still owned by the client the user passed to
|
|
2019
|
+
* the adapter at construction time.
|
|
2020
|
+
*/
|
|
2021
|
+
async scanByUrl(url, options) {
|
|
2022
|
+
const scheme = parseUrlScheme(url);
|
|
2023
|
+
return this.registry.getByUrlScheme(scheme).introspector.introspect(options);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Typed query entry point. Returns a namespace-level Proxy over the
|
|
2027
|
+
* given `DataModel`:
|
|
2028
|
+
*
|
|
2029
|
+
* biref.query(model).public.users
|
|
2030
|
+
* .select('id', 'email')
|
|
2031
|
+
* .where('active', 'eq', true)
|
|
2032
|
+
* .include('orders', (q) => q.select('id', 'total'))
|
|
2033
|
+
* .findMany();
|
|
2034
|
+
*
|
|
2035
|
+
* The Proxy layers (`namespace` → `entity`) enumerate what's
|
|
2036
|
+
* actually in the scanned model, so `Object.keys(biref.query(model))`
|
|
2037
|
+
* returns the discovered namespaces. Access to a non-existent
|
|
2038
|
+
* namespace or entity returns `undefined`; the chain methods throw
|
|
2039
|
+
* with a clear error when given unknown fields or relations.
|
|
2040
|
+
*
|
|
2041
|
+
* With a single registered adapter, the adapter is picked
|
|
2042
|
+
* automatically. Pass `adapterName` explicitly to target a specific
|
|
2043
|
+
* adapter when more than one is registered.
|
|
2044
|
+
*/
|
|
2045
|
+
query(model, adapterName) {
|
|
2046
|
+
const adapter = adapterName ? this.registry.get(adapterName) : this.requireSingleAdapter("query");
|
|
2047
|
+
const executor = this.executorFor(adapter);
|
|
2048
|
+
const proxy = createNamespaceProxy({ model, executor });
|
|
2049
|
+
return proxy;
|
|
2050
|
+
}
|
|
2051
|
+
/** Direct access to the underlying registry. */
|
|
2052
|
+
get adapters() {
|
|
2053
|
+
return this.registry;
|
|
2054
|
+
}
|
|
2055
|
+
executorFor(adapter) {
|
|
2056
|
+
if (!adapter.runner) {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
`Adapter "${adapter.name}" does not expose a RawQueryRunner. Update the adapter factory to attach one before calling biref.query(...).`
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
if (!adapter.parser) {
|
|
2062
|
+
throw new Error(
|
|
2063
|
+
`Adapter "${adapter.name}" does not expose a RecordParser. Update the adapter factory to attach one before calling biref.query(...).`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
return new QueryPlanExecutor(
|
|
2067
|
+
adapter.engine,
|
|
2068
|
+
adapter.runner,
|
|
2069
|
+
adapter.parser
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
resolveScanArgs(adapterNameOrOptions, maybeOptions) {
|
|
2073
|
+
if (typeof adapterNameOrOptions === "string") {
|
|
2074
|
+
return {
|
|
2075
|
+
adapter: this.registry.get(adapterNameOrOptions),
|
|
2076
|
+
options: maybeOptions
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
adapter: this.requireSingleAdapter("scan"),
|
|
2081
|
+
options: adapterNameOrOptions
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
requireSingleAdapter(operation) {
|
|
2085
|
+
const names = this.registry.list();
|
|
2086
|
+
if (names.length === 0) {
|
|
2087
|
+
throw new Error(
|
|
2088
|
+
`Biref.${operation}() called with no adapter argument, but no adapters are registered. Register one via Biref.builder().withAdapter(...).`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
if (names.length > 1) {
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
`Biref.${operation}() called with no adapter argument, but multiple adapters are registered (${names.join(", ")}). Pass an adapter name explicitly.`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
const onlyName = names[0];
|
|
2097
|
+
if (onlyName === void 0) {
|
|
2098
|
+
throw new Error(
|
|
2099
|
+
"Internal error: adapter list reported a length of 1 but produced no name."
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
return this.registry.get(onlyName);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
var BirefBuilder = class {
|
|
2106
|
+
pending = [];
|
|
2107
|
+
/**
|
|
2108
|
+
* Register an adapter. The adapter's `name` is used as the lookup
|
|
2109
|
+
* key when calling `Biref.scan(name)`. Its optional `urlSchemes` are
|
|
2110
|
+
* used by `Biref.scanByUrl(url)`.
|
|
2111
|
+
*/
|
|
2112
|
+
withAdapter(adapter) {
|
|
2113
|
+
this.pending.push(adapter);
|
|
2114
|
+
return this;
|
|
2115
|
+
}
|
|
2116
|
+
/** Convenience for registering multiple adapters in one call. */
|
|
2117
|
+
withAdapters(...adapters) {
|
|
2118
|
+
for (const adapter of adapters) {
|
|
2119
|
+
this.pending.push(adapter);
|
|
2120
|
+
}
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Materialize the builder into a `Biref` facade. Throws if any
|
|
2125
|
+
* adapter has a duplicate name.
|
|
2126
|
+
*/
|
|
2127
|
+
build() {
|
|
2128
|
+
const registry = new AdapterRegistry();
|
|
2129
|
+
for (const adapter of this.pending) {
|
|
2130
|
+
registry.register(adapter);
|
|
2131
|
+
}
|
|
2132
|
+
return new Biref(registry);
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
function parseUrlScheme(url) {
|
|
2136
|
+
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
|
|
2137
|
+
if (!match?.[1]) {
|
|
2138
|
+
throw new Error(`Cannot determine driver scheme from URL: "${url}"`);
|
|
2139
|
+
}
|
|
2140
|
+
return match[1].toLowerCase();
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// src/core/Scanner.ts
|
|
2144
|
+
var Scanner = class {
|
|
2145
|
+
constructor(registry) {
|
|
2146
|
+
this.registry = registry;
|
|
2147
|
+
}
|
|
2148
|
+
registry;
|
|
2149
|
+
/**
|
|
2150
|
+
* Scan a data store using the named adapter and return a paradigm-
|
|
2151
|
+
* neutral `DataModel`.
|
|
2152
|
+
*/
|
|
2153
|
+
async scan(adapterName, options) {
|
|
2154
|
+
return this.registry.get(adapterName).introspector.introspect(options);
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
|
|
2158
|
+
// src/output/CsvFormatter.ts
|
|
2159
|
+
var CsvFormatter = class {
|
|
2160
|
+
constructor(options = {}) {
|
|
2161
|
+
this.options = options;
|
|
2162
|
+
}
|
|
2163
|
+
options;
|
|
2164
|
+
format = "csv";
|
|
2165
|
+
contentType = "text/csv";
|
|
2166
|
+
serialize(records, options) {
|
|
2167
|
+
const delimiter = this.options.delimiter ?? ",";
|
|
2168
|
+
const lineTerminator = this.options.lineTerminator ?? "\n";
|
|
2169
|
+
const includeHeader = this.options.includeHeader ?? true;
|
|
2170
|
+
const fields = this.resolveFields(records, options);
|
|
2171
|
+
if (fields.length === 0) {
|
|
2172
|
+
return "";
|
|
2173
|
+
}
|
|
2174
|
+
const lines = [];
|
|
2175
|
+
if (includeHeader) {
|
|
2176
|
+
lines.push(fields.map((f) => this.escape(f, delimiter)).join(delimiter));
|
|
2177
|
+
}
|
|
2178
|
+
for (const record of records) {
|
|
2179
|
+
const cells = fields.map(
|
|
2180
|
+
(field) => this.serializeCell(record[field], delimiter)
|
|
2181
|
+
);
|
|
2182
|
+
lines.push(cells.join(delimiter));
|
|
2183
|
+
}
|
|
2184
|
+
return lines.join(lineTerminator);
|
|
2185
|
+
}
|
|
2186
|
+
resolveFields(records, options) {
|
|
2187
|
+
if (options?.fields && options.fields.length > 0) {
|
|
2188
|
+
return options.fields;
|
|
2189
|
+
}
|
|
2190
|
+
const first = records[0];
|
|
2191
|
+
return first ? Object.keys(first) : [];
|
|
2192
|
+
}
|
|
2193
|
+
serializeCell(value, delimiter) {
|
|
2194
|
+
if (value === null || value === void 0) {
|
|
2195
|
+
return "";
|
|
2196
|
+
}
|
|
2197
|
+
if (value instanceof Date) {
|
|
2198
|
+
return this.escape(value.toISOString(), delimiter);
|
|
2199
|
+
}
|
|
2200
|
+
if (typeof value === "bigint") {
|
|
2201
|
+
return this.escape(value.toString(), delimiter);
|
|
2202
|
+
}
|
|
2203
|
+
if (typeof value === "object") {
|
|
2204
|
+
return this.escape(JSON.stringify(value), delimiter);
|
|
2205
|
+
}
|
|
2206
|
+
return this.escape(String(value), delimiter);
|
|
2207
|
+
}
|
|
2208
|
+
escape(value, delimiter) {
|
|
2209
|
+
if (value.includes(delimiter) || value.includes('"') || value.includes("\n") || value.includes("\r")) {
|
|
2210
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
2211
|
+
}
|
|
2212
|
+
return value;
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
// src/output/JsonFormatter.ts
|
|
2217
|
+
var JsonFormatter = class _JsonFormatter {
|
|
2218
|
+
constructor(options = {}) {
|
|
2219
|
+
this.options = options;
|
|
2220
|
+
}
|
|
2221
|
+
options;
|
|
2222
|
+
format = "json";
|
|
2223
|
+
contentType = "application/json";
|
|
2224
|
+
serialize(records) {
|
|
2225
|
+
const indent2 = this.options.pretty ? 2 : void 0;
|
|
2226
|
+
return JSON.stringify(records, _JsonFormatter.replacer, indent2);
|
|
2227
|
+
}
|
|
2228
|
+
static replacer(_key, value) {
|
|
2229
|
+
if (value instanceof Date) {
|
|
2230
|
+
return value.toISOString();
|
|
2231
|
+
}
|
|
2232
|
+
if (typeof value === "bigint") {
|
|
2233
|
+
return value.toString();
|
|
2234
|
+
}
|
|
2235
|
+
return value;
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
// src/output/RawFormatter.ts
|
|
2240
|
+
var RawFormatter = class {
|
|
2241
|
+
format = "raw";
|
|
2242
|
+
contentType = "application/javascript";
|
|
2243
|
+
serialize(records) {
|
|
2244
|
+
return records;
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
|
|
2248
|
+
exports.AdapterRegistry = AdapterRegistry;
|
|
2249
|
+
exports.Biref = Biref;
|
|
2250
|
+
exports.BirefBuilder = BirefBuilder;
|
|
2251
|
+
exports.ChainBuilder = ChainBuilder;
|
|
2252
|
+
exports.CsvFormatter = CsvFormatter;
|
|
2253
|
+
exports.DataModel = DataModel;
|
|
2254
|
+
exports.DefaultRecordParser = DefaultRecordParser;
|
|
2255
|
+
exports.JsonFormatter = JsonFormatter;
|
|
2256
|
+
exports.POSTGRES_ADAPTER_NAME = POSTGRES_ADAPTER_NAME;
|
|
2257
|
+
exports.POSTGRES_URL_SCHEMES = POSTGRES_URL_SCHEMES;
|
|
2258
|
+
exports.PostgresIntrospector = PostgresIntrospector;
|
|
2259
|
+
exports.PostgresQueryEngine = PostgresQueryEngine;
|
|
2260
|
+
exports.PostgresRecordParser = PostgresRecordParser;
|
|
2261
|
+
exports.QueryPlanExecutor = QueryPlanExecutor;
|
|
2262
|
+
exports.RawFormatter = RawFormatter;
|
|
2263
|
+
exports.Scanner = Scanner;
|
|
2264
|
+
exports.generateSchema = generateSchema;
|
|
2265
|
+
exports.generateSchemaFiles = generateSchemaFiles;
|
|
2266
|
+
exports.isPostgresAdapter = isPostgresAdapter;
|
|
2267
|
+
exports.overridesScaffold = overridesScaffold;
|
|
2268
|
+
exports.postgresAdapter = postgresAdapter;
|
|
2269
|
+
exports.tsTypeFor = tsTypeFor;
|
|
2270
|
+
//# sourceMappingURL=index.cjs.map
|
|
2271
|
+
//# sourceMappingURL=index.cjs.map
|