@rangka/core 0.1.0 → 0.1.2
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/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -18
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
package/src/db/introspect.ts
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from 'kysely';
|
|
2
|
-
import { sql } from 'kysely';
|
|
3
|
-
import type {
|
|
4
|
-
ActualState,
|
|
5
|
-
ActualTable,
|
|
6
|
-
ActualColumn,
|
|
7
|
-
ActualForeignKey,
|
|
8
|
-
ActualIndex,
|
|
9
|
-
ActualCheckConstraint,
|
|
10
|
-
} from './types.js';
|
|
11
|
-
|
|
12
|
-
// ─── Public Entry Point ──────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Introspects the current database schema by querying PostgreSQL system catalogs.
|
|
16
|
-
* Returns the actual state of all public tables, columns, foreign keys, indexes,
|
|
17
|
-
* and check constraints.
|
|
18
|
-
*/
|
|
19
|
-
export async function introspect(db: Kysely<unknown>): Promise<ActualState> {
|
|
20
|
-
const tables = await introspectTables(db);
|
|
21
|
-
return { tables };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ─── Table Discovery ─────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
/** Fetches all base tables in the public schema and introspects each one. */
|
|
27
|
-
async function introspectTables(db: Kysely<unknown>): Promise<ActualTable[]> {
|
|
28
|
-
const { rows: tableRows } = await sql<{ table_name: string }>`
|
|
29
|
-
SELECT table_name FROM information_schema.tables
|
|
30
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
31
|
-
`.execute(db);
|
|
32
|
-
|
|
33
|
-
const tables: ActualTable[] = [];
|
|
34
|
-
|
|
35
|
-
for (const row of tableRows) {
|
|
36
|
-
const columns = await introspectColumns(db, row.table_name);
|
|
37
|
-
const foreignKeys = await introspectForeignKeys(db, row.table_name);
|
|
38
|
-
const indexes = await introspectIndexes(db, row.table_name);
|
|
39
|
-
const checkConstraints = await introspectCheckConstraints(db, row.table_name);
|
|
40
|
-
|
|
41
|
-
tables.push({
|
|
42
|
-
name: row.table_name,
|
|
43
|
-
columns,
|
|
44
|
-
foreignKeys,
|
|
45
|
-
indexes,
|
|
46
|
-
checkConstraints,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return tables;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ─── Column Introspection ────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
interface ColumnRow {
|
|
56
|
-
column_name: string;
|
|
57
|
-
data_type: string;
|
|
58
|
-
is_nullable: string;
|
|
59
|
-
column_default: string | null;
|
|
60
|
-
character_maximum_length: number | null;
|
|
61
|
-
numeric_precision: number | null;
|
|
62
|
-
numeric_scale: number | null;
|
|
63
|
-
udt_name: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Reads column metadata from information_schema for a given table. */
|
|
67
|
-
async function introspectColumns(db: Kysely<unknown>, tableName: string): Promise<ActualColumn[]> {
|
|
68
|
-
const { rows: columnRows } = await sql<ColumnRow>`
|
|
69
|
-
SELECT column_name, data_type, is_nullable, column_default,
|
|
70
|
-
character_maximum_length, numeric_precision, numeric_scale, udt_name
|
|
71
|
-
FROM information_schema.columns
|
|
72
|
-
WHERE table_schema = 'public' AND table_name = ${tableName}
|
|
73
|
-
ORDER BY ordinal_position
|
|
74
|
-
`.execute(db);
|
|
75
|
-
|
|
76
|
-
return columnRows.map((row) => ({
|
|
77
|
-
name: row.column_name,
|
|
78
|
-
type: normalizeType(row),
|
|
79
|
-
nullable: row.is_nullable === 'YES',
|
|
80
|
-
defaultValue: normalizeDefault(row.column_default),
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── Foreign Key Introspection ───────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
interface ForeignKeyRow {
|
|
87
|
-
name: string;
|
|
88
|
-
column_name: string;
|
|
89
|
-
referenced_table: string;
|
|
90
|
-
referenced_column: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Reads foreign key relationships for a given table. */
|
|
94
|
-
async function introspectForeignKeys(
|
|
95
|
-
db: Kysely<unknown>,
|
|
96
|
-
tableName: string,
|
|
97
|
-
): Promise<ActualForeignKey[]> {
|
|
98
|
-
const { rows: foreignKeyRows } = await sql<ForeignKeyRow>`
|
|
99
|
-
SELECT tc.constraint_name AS name,
|
|
100
|
-
kcu.column_name AS column_name,
|
|
101
|
-
ccu.table_name AS referenced_table,
|
|
102
|
-
ccu.column_name AS referenced_column
|
|
103
|
-
FROM information_schema.table_constraints tc
|
|
104
|
-
JOIN information_schema.key_column_usage kcu
|
|
105
|
-
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
106
|
-
JOIN information_schema.constraint_column_usage ccu
|
|
107
|
-
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
|
108
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
109
|
-
AND tc.table_name = ${tableName}
|
|
110
|
-
AND tc.table_schema = 'public'
|
|
111
|
-
`.execute(db);
|
|
112
|
-
|
|
113
|
-
return foreignKeyRows.map((row) => ({
|
|
114
|
-
name: row.name,
|
|
115
|
-
column: row.column_name,
|
|
116
|
-
referencedTable: row.referenced_table,
|
|
117
|
-
referencedColumn: row.referenced_column,
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ─── Index Introspection ─────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
interface IndexRow {
|
|
124
|
-
indexname: string;
|
|
125
|
-
indexdef: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Reads non-primary-key indexes for a given table from pg_indexes. */
|
|
129
|
-
async function introspectIndexes(db: Kysely<unknown>, tableName: string): Promise<ActualIndex[]> {
|
|
130
|
-
const { rows: indexRows } = await sql<IndexRow>`
|
|
131
|
-
SELECT indexname, indexdef FROM pg_indexes
|
|
132
|
-
WHERE schemaname = 'public' AND tablename = ${tableName}
|
|
133
|
-
`.execute(db);
|
|
134
|
-
|
|
135
|
-
// Filter out primary key indexes (they're implicit, not user-defined)
|
|
136
|
-
return indexRows
|
|
137
|
-
.filter((row) => !row.indexname.endsWith('_pkey'))
|
|
138
|
-
.map((row) => ({
|
|
139
|
-
name: row.indexname,
|
|
140
|
-
columns: parseIndexColumns(row.indexdef),
|
|
141
|
-
unique: row.indexdef.toUpperCase().includes('UNIQUE'),
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ─── Check Constraint Introspection ──────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
interface CheckConstraintRow {
|
|
148
|
-
name: string;
|
|
149
|
-
expression: string;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Reads check constraints for a given table, excluding NOT NULL constraints. */
|
|
153
|
-
async function introspectCheckConstraints(
|
|
154
|
-
db: Kysely<unknown>,
|
|
155
|
-
tableName: string,
|
|
156
|
-
): Promise<ActualCheckConstraint[]> {
|
|
157
|
-
const { rows: constraintRows } = await sql<CheckConstraintRow>`
|
|
158
|
-
SELECT tc.constraint_name AS name,
|
|
159
|
-
cc.check_clause AS expression
|
|
160
|
-
FROM information_schema.table_constraints tc
|
|
161
|
-
JOIN information_schema.check_constraints cc
|
|
162
|
-
ON cc.constraint_name = tc.constraint_name AND cc.constraint_schema = tc.constraint_schema
|
|
163
|
-
WHERE tc.constraint_type = 'CHECK'
|
|
164
|
-
AND tc.table_name = ${tableName}
|
|
165
|
-
AND tc.table_schema = 'public'
|
|
166
|
-
`.execute(db);
|
|
167
|
-
|
|
168
|
-
// NOT NULL constraints show up as check constraints; filter them out
|
|
169
|
-
return constraintRows
|
|
170
|
-
.filter((row) => !row.name.endsWith('_not_null'))
|
|
171
|
-
.map((row) => ({
|
|
172
|
-
name: row.name,
|
|
173
|
-
column: extractColumnFromCheck(row.expression),
|
|
174
|
-
expression: row.expression,
|
|
175
|
-
}));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Type Normalization Helpers ──────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Converts a PostgreSQL data_type (from information_schema) into a canonical
|
|
182
|
-
* uppercase SQL type string used for schema comparison.
|
|
183
|
-
*/
|
|
184
|
-
function normalizeType(row: ColumnRow): string {
|
|
185
|
-
const dataType = row.data_type.toLowerCase();
|
|
186
|
-
|
|
187
|
-
switch (dataType) {
|
|
188
|
-
case 'character varying':
|
|
189
|
-
return `VARCHAR(${row.character_maximum_length ?? 255})`;
|
|
190
|
-
case 'character':
|
|
191
|
-
return `CHAR(${row.character_maximum_length ?? 1})`;
|
|
192
|
-
case 'integer':
|
|
193
|
-
return 'INTEGER';
|
|
194
|
-
case 'bigint':
|
|
195
|
-
return 'BIGINT';
|
|
196
|
-
case 'smallint':
|
|
197
|
-
return 'SMALLINT';
|
|
198
|
-
case 'numeric':
|
|
199
|
-
return `DECIMAL(${row.numeric_precision ?? 18},${row.numeric_scale ?? 6})`;
|
|
200
|
-
case 'boolean':
|
|
201
|
-
return 'BOOLEAN';
|
|
202
|
-
case 'text':
|
|
203
|
-
return 'TEXT';
|
|
204
|
-
case 'date':
|
|
205
|
-
return 'DATE';
|
|
206
|
-
case 'timestamp with time zone':
|
|
207
|
-
return 'TIMESTAMPTZ';
|
|
208
|
-
case 'timestamp without time zone':
|
|
209
|
-
return 'TIMESTAMP';
|
|
210
|
-
case 'jsonb':
|
|
211
|
-
return 'JSONB';
|
|
212
|
-
case 'json':
|
|
213
|
-
return 'JSON';
|
|
214
|
-
case 'uuid':
|
|
215
|
-
return 'UUID';
|
|
216
|
-
case 'user-defined':
|
|
217
|
-
return row.udt_name.toUpperCase();
|
|
218
|
-
default:
|
|
219
|
-
return dataType.toUpperCase();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Cleans up a column_default value from information_schema:
|
|
225
|
-
* - Strips sequence references (nextval) since auto-increment is handled separately
|
|
226
|
-
* - Removes PostgreSQL type cast suffixes (e.g., 'value'::text -> 'value')
|
|
227
|
-
* - Normalizes boolean literals to uppercase
|
|
228
|
-
*/
|
|
229
|
-
function normalizeDefault(rawDefault: string | null): string | null {
|
|
230
|
-
if (rawDefault === null) return null;
|
|
231
|
-
|
|
232
|
-
// Auto-increment columns use nextval(); treat them as having no explicit default
|
|
233
|
-
if (rawDefault.startsWith('nextval(')) return null;
|
|
234
|
-
|
|
235
|
-
// Strip type cast suffix: 'some_value'::character varying -> 'some_value'
|
|
236
|
-
const castMatch = rawDefault.match(/^'([^']*)'::[\w\s]+$/);
|
|
237
|
-
if (castMatch) return `'${castMatch[1]}'`;
|
|
238
|
-
|
|
239
|
-
// Normalize boolean literals to uppercase for consistent comparison
|
|
240
|
-
if (rawDefault === 'true') return 'TRUE';
|
|
241
|
-
if (rawDefault === 'false') return 'FALSE';
|
|
242
|
-
|
|
243
|
-
return rawDefault;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ─── Index/Constraint Parsing Helpers ────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Extracts column names from a CREATE INDEX definition string.
|
|
250
|
-
* Example: "CREATE INDEX idx ON tbl (col1, col2)" -> ["col1", "col2"]
|
|
251
|
-
*/
|
|
252
|
-
function parseIndexColumns(indexDefinition: string): string[] {
|
|
253
|
-
const match = indexDefinition.match(/\(([^)]+)\)/);
|
|
254
|
-
if (!match) return [];
|
|
255
|
-
return match[1].split(',').map((col) => col.trim().replace(/"/g, ''));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Extracts the first column name referenced in a CHECK constraint expression.
|
|
260
|
-
* Example: "(age > 0)" -> "age"
|
|
261
|
-
*/
|
|
262
|
-
function extractColumnFromCheck(expression: string): string {
|
|
263
|
-
const match = expression.match(/^\(?(\w+)/);
|
|
264
|
-
return match ? match[1] : '';
|
|
265
|
-
}
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { Kysely } from 'kysely';
|
|
3
|
-
import type { SchemaRegistry } from '../schema/registry.js';
|
|
4
|
-
import type { ModelRelationship } from '../schema/types.js';
|
|
5
|
-
import { modelToTableName } from './field-mapper.js';
|
|
6
|
-
import type { AdapterRegistry } from '../plugins/adapter-registry.js';
|
|
7
|
-
import type { ExternalFieldConfig } from '../external-model/types.js';
|
|
8
|
-
import { mapAdapterResponse } from '../external-model/field-mapper.js';
|
|
9
|
-
import { evaluateComputedFields } from '../external-model/computed-fields.js';
|
|
10
|
-
|
|
11
|
-
export type IncludeSpec = string | { relation: string; nested?: IncludeSpec[] };
|
|
12
|
-
|
|
13
|
-
export interface IncludeResolverOptions {
|
|
14
|
-
adapterRegistry?: AdapterRegistry;
|
|
15
|
-
externalModelFields?: Record<string, Record<string, ExternalFieldConfig>>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeSpec(spec: IncludeSpec): { relation: string; nested: IncludeSpec[] } {
|
|
19
|
-
if (typeof spec === 'string') return { relation: spec, nested: [] };
|
|
20
|
-
return { relation: spec.relation, nested: spec.nested ?? [] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function resolveModelIncludes(
|
|
24
|
-
records: Record<string, unknown>[],
|
|
25
|
-
includes: IncludeSpec[],
|
|
26
|
-
registry: SchemaRegistry,
|
|
27
|
-
db: Kysely<any>,
|
|
28
|
-
sourceModel: string,
|
|
29
|
-
options?: IncludeResolverOptions,
|
|
30
|
-
): Promise<void> {
|
|
31
|
-
if (records.length === 0) return;
|
|
32
|
-
|
|
33
|
-
const relationships = registry.getRelationshipsForModel(sourceModel);
|
|
34
|
-
|
|
35
|
-
for (const spec of includes) {
|
|
36
|
-
const { relation, nested } = normalizeSpec(spec);
|
|
37
|
-
const rel = relationships.find((r) => r.field === relation);
|
|
38
|
-
if (!rel) continue;
|
|
39
|
-
|
|
40
|
-
const targetModel = registry.getModel(rel.to);
|
|
41
|
-
const isExternal = targetModel?.source != null;
|
|
42
|
-
|
|
43
|
-
if (isExternal && options?.adapterRegistry) {
|
|
44
|
-
switch (rel.type) {
|
|
45
|
-
case 'link':
|
|
46
|
-
await resolveExternalLink(
|
|
47
|
-
records,
|
|
48
|
-
rel,
|
|
49
|
-
targetModel!.source!,
|
|
50
|
-
targetModel!.qualifiedName,
|
|
51
|
-
options,
|
|
52
|
-
);
|
|
53
|
-
break;
|
|
54
|
-
case 'hasMany':
|
|
55
|
-
case 'children':
|
|
56
|
-
await resolveExternalHasMany(
|
|
57
|
-
records,
|
|
58
|
-
rel,
|
|
59
|
-
targetModel!.source!,
|
|
60
|
-
targetModel!.qualifiedName,
|
|
61
|
-
options,
|
|
62
|
-
);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
switch (rel.type) {
|
|
67
|
-
case 'link':
|
|
68
|
-
await resolveLink(records, rel, db);
|
|
69
|
-
break;
|
|
70
|
-
case 'hasMany':
|
|
71
|
-
case 'children':
|
|
72
|
-
await resolveHasMany(records, rel, db);
|
|
73
|
-
break;
|
|
74
|
-
case 'manyToMany':
|
|
75
|
-
await resolveManyToMany(records, rel, db);
|
|
76
|
-
break;
|
|
77
|
-
case 'dynamicLink':
|
|
78
|
-
await resolveDynamicLink(records, rel, db);
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (nested.length > 0) {
|
|
84
|
-
const childRecords = records
|
|
85
|
-
.map((r) => r[rel.field])
|
|
86
|
-
.flat()
|
|
87
|
-
.filter((r): r is Record<string, unknown> => r != null && typeof r === 'object');
|
|
88
|
-
|
|
89
|
-
if (childRecords.length > 0) {
|
|
90
|
-
await resolveModelIncludes(childRecords, nested, registry, db, rel.to, options);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function resolveLink(
|
|
97
|
-
records: Record<string, unknown>[],
|
|
98
|
-
rel: ModelRelationship,
|
|
99
|
-
db: Kysely<any>,
|
|
100
|
-
): Promise<void> {
|
|
101
|
-
const ids = [
|
|
102
|
-
...new Set(
|
|
103
|
-
records
|
|
104
|
-
.map((r) => r[rel.field])
|
|
105
|
-
.filter((id): id is string => id != null && typeof id === 'string'),
|
|
106
|
-
),
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
if (ids.length === 0) {
|
|
110
|
-
for (const record of records) {
|
|
111
|
-
if (record[rel.field] == null) record[rel.field] = null;
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const table = modelToTableName(rel.to);
|
|
117
|
-
const related = await db.selectFrom(table).selectAll().where('id', 'in', ids).execute();
|
|
118
|
-
|
|
119
|
-
const map = new Map<string, Record<string, unknown>>();
|
|
120
|
-
for (const row of related) {
|
|
121
|
-
map.set((row as any).id, row as Record<string, unknown>);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const record of records) {
|
|
125
|
-
const id = record[rel.field] as string | null;
|
|
126
|
-
record[rel.field] = id ? (map.get(id) ?? null) : null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function resolveHasMany(
|
|
131
|
-
records: Record<string, unknown>[],
|
|
132
|
-
rel: ModelRelationship,
|
|
133
|
-
db: Kysely<any>,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const parentIds = records.map((r) => r.id as string).filter(Boolean);
|
|
136
|
-
if (parentIds.length === 0) return;
|
|
137
|
-
|
|
138
|
-
const table = modelToTableName(rel.to);
|
|
139
|
-
const foreignKey = rel.foreignKey!;
|
|
140
|
-
|
|
141
|
-
const related = await db
|
|
142
|
-
.selectFrom(table)
|
|
143
|
-
.selectAll()
|
|
144
|
-
.where(foreignKey, 'in', parentIds)
|
|
145
|
-
.execute();
|
|
146
|
-
|
|
147
|
-
const grouped = new Map<string, Record<string, unknown>[]>();
|
|
148
|
-
for (const row of related as any[]) {
|
|
149
|
-
const fkValue = row[foreignKey] as string;
|
|
150
|
-
const list = grouped.get(fkValue) ?? [];
|
|
151
|
-
list.push(row);
|
|
152
|
-
grouped.set(fkValue, list);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const record of records) {
|
|
156
|
-
record[rel.field] = grouped.get(record.id as string) ?? [];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function resolveManyToMany(
|
|
161
|
-
records: Record<string, unknown>[],
|
|
162
|
-
rel: ModelRelationship,
|
|
163
|
-
db: Kysely<any>,
|
|
164
|
-
): Promise<void> {
|
|
165
|
-
const parentIds = records.map((r) => r.id as string).filter(Boolean);
|
|
166
|
-
if (parentIds.length === 0) return;
|
|
167
|
-
|
|
168
|
-
const targetTable = modelToTableName(rel.to);
|
|
169
|
-
const junctionTable = modelToTableName(rel.through!);
|
|
170
|
-
|
|
171
|
-
const sourceModelName = rel.from.split('.').pop()!;
|
|
172
|
-
const targetModelName = rel.to.split('.').pop()!;
|
|
173
|
-
const sourceFk = `${sourceModelName}_id`;
|
|
174
|
-
const targetFk = `${targetModelName}_id`;
|
|
175
|
-
|
|
176
|
-
const related = await db
|
|
177
|
-
.selectFrom(targetTable)
|
|
178
|
-
.selectAll(targetTable)
|
|
179
|
-
.select(`${junctionTable}.${sourceFk} as _source_fk`)
|
|
180
|
-
.innerJoin(junctionTable, `${junctionTable}.${targetFk}`, `${targetTable}.id`)
|
|
181
|
-
.where(`${junctionTable}.${sourceFk}`, 'in', parentIds)
|
|
182
|
-
.execute();
|
|
183
|
-
|
|
184
|
-
const grouped = new Map<string, Record<string, unknown>[]>();
|
|
185
|
-
for (const row of related as any[]) {
|
|
186
|
-
const fkValue = row._source_fk as string;
|
|
187
|
-
const clean = { ...row };
|
|
188
|
-
delete clean._source_fk;
|
|
189
|
-
const list = grouped.get(fkValue) ?? [];
|
|
190
|
-
list.push(clean);
|
|
191
|
-
grouped.set(fkValue, list);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (const record of records) {
|
|
195
|
-
record[rel.field] = grouped.get(record.id as string) ?? [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function resolveDynamicLink(
|
|
200
|
-
records: Record<string, unknown>[],
|
|
201
|
-
rel: ModelRelationship,
|
|
202
|
-
db: Kysely<any>,
|
|
203
|
-
): Promise<void> {
|
|
204
|
-
const modelField = rel.modelField!;
|
|
205
|
-
|
|
206
|
-
const groups = new Map<string, { records: Record<string, unknown>[]; ids: string[] }>();
|
|
207
|
-
for (const record of records) {
|
|
208
|
-
const targetModel = record[modelField] as string | null;
|
|
209
|
-
const id = record[rel.field] as string | null;
|
|
210
|
-
if (!targetModel || !id) {
|
|
211
|
-
record[rel.field] = null;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const group = groups.get(targetModel) ?? { records: [], ids: [] };
|
|
215
|
-
group.records.push(record);
|
|
216
|
-
group.ids.push(id);
|
|
217
|
-
groups.set(targetModel, group);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for (const [targetModel, group] of groups) {
|
|
221
|
-
const table = modelToTableName(targetModel);
|
|
222
|
-
const uniqueIds = [...new Set(group.ids)];
|
|
223
|
-
|
|
224
|
-
const related = await db.selectFrom(table).selectAll().where('id', 'in', uniqueIds).execute();
|
|
225
|
-
|
|
226
|
-
const map = new Map<string, Record<string, unknown>>();
|
|
227
|
-
for (const row of related as any[]) {
|
|
228
|
-
map.set(row.id, row);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
for (const record of group.records) {
|
|
232
|
-
const id = record[rel.field] as string;
|
|
233
|
-
record[rel.field] = map.get(id) ?? null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function resolveExternalLink(
|
|
239
|
-
records: Record<string, unknown>[],
|
|
240
|
-
rel: ModelRelationship,
|
|
241
|
-
adapterSource: string,
|
|
242
|
-
qualifiedName: string,
|
|
243
|
-
options: IncludeResolverOptions,
|
|
244
|
-
): Promise<void> {
|
|
245
|
-
const ids = [
|
|
246
|
-
...new Set(
|
|
247
|
-
records
|
|
248
|
-
.map((r) => r[rel.field])
|
|
249
|
-
.filter((id): id is string => id != null && typeof id === 'string'),
|
|
250
|
-
),
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
if (ids.length === 0) {
|
|
254
|
-
for (const record of records) {
|
|
255
|
-
if (record[rel.field] == null) record[rel.field] = null;
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const adapter = options.adapterRegistry!.get(adapterSource);
|
|
261
|
-
const fields = options.externalModelFields?.[qualifiedName] ?? {};
|
|
262
|
-
|
|
263
|
-
let related: Record<string, unknown>[];
|
|
264
|
-
if (adapter.batchGet) {
|
|
265
|
-
related = await adapter.batchGet(qualifiedName, ids);
|
|
266
|
-
} else {
|
|
267
|
-
const results = await Promise.all(ids.map((id) => adapter.get(qualifiedName, id)));
|
|
268
|
-
related = results.filter((r): r is Record<string, unknown> => r != null);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const mapped = related.map((raw) => {
|
|
272
|
-
const m = mapAdapterResponse(raw, fields);
|
|
273
|
-
return evaluateComputedFields(m, fields);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const map = new Map<string, Record<string, unknown>>();
|
|
277
|
-
for (const row of mapped) {
|
|
278
|
-
if (row.id != null) map.set(row.id as string, row);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
for (const record of records) {
|
|
282
|
-
const id = record[rel.field] as string | null;
|
|
283
|
-
record[rel.field] = id ? (map.get(id) ?? null) : null;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function resolveExternalHasMany(
|
|
288
|
-
records: Record<string, unknown>[],
|
|
289
|
-
rel: ModelRelationship,
|
|
290
|
-
adapterSource: string,
|
|
291
|
-
qualifiedName: string,
|
|
292
|
-
options: IncludeResolverOptions,
|
|
293
|
-
): Promise<void> {
|
|
294
|
-
const parentIds = records.map((r) => r.id as string).filter(Boolean);
|
|
295
|
-
if (parentIds.length === 0) return;
|
|
296
|
-
|
|
297
|
-
const adapter = options.adapterRegistry!.get(adapterSource);
|
|
298
|
-
const fields = options.externalModelFields?.[qualifiedName] ?? {};
|
|
299
|
-
const foreignKey = rel.foreignKey!;
|
|
300
|
-
|
|
301
|
-
let allRelated: Record<string, unknown>[] = [];
|
|
302
|
-
|
|
303
|
-
if (adapter.list) {
|
|
304
|
-
const result = await adapter.list(qualifiedName, {
|
|
305
|
-
filters: [{ field: foreignKey, operator: 'in', value: parentIds }],
|
|
306
|
-
});
|
|
307
|
-
allRelated = result.data;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const mapped = allRelated.map((raw) => {
|
|
311
|
-
const m = mapAdapterResponse(raw, fields);
|
|
312
|
-
return evaluateComputedFields(m, fields);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const grouped = new Map<string, Record<string, unknown>[]>();
|
|
316
|
-
for (const row of mapped) {
|
|
317
|
-
const fkValue = row[foreignKey] as string;
|
|
318
|
-
if (!fkValue) continue;
|
|
319
|
-
const list = grouped.get(fkValue) ?? [];
|
|
320
|
-
list.push(row);
|
|
321
|
-
grouped.set(fkValue, list);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
for (const record of records) {
|
|
325
|
-
record[rel.field] = grouped.get(record.id as string) ?? [];
|
|
326
|
-
}
|
|
327
|
-
}
|