@travetto/model-sql 8.0.0-alpha.25 → 8.0.0-alpha.26

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.
@@ -1,64 +0,0 @@
1
- import type { SchemaClassConfig, SchemaFieldConfig } from '@travetto/schema';
2
-
3
- import type { VisitStack } from '../types.ts';
4
-
5
- /**
6
- * Insertion wrapper
7
- */
8
- export interface InsertWrapper {
9
- stack: VisitStack[];
10
- records: { stack: VisitStack[]; value: unknown }[];
11
- }
12
-
13
- /**
14
- * Dialect wrapper
15
- */
16
- export interface DeleteWrapper {
17
- stack: VisitStack[];
18
- ids: string[];
19
- }
20
-
21
- /**
22
- * Dialect state
23
- */
24
- export interface DialectState {
25
- pathField: SchemaFieldConfig;
26
- parentPathField: SchemaFieldConfig;
27
- idField: SchemaFieldConfig;
28
- idxField: SchemaFieldConfig;
29
- }
30
-
31
- export type VisitState = { path: VisitStack[] };
32
-
33
- /**
34
- * Visited node
35
- */
36
- export interface VisitNode<R> {
37
- path: VisitStack[];
38
- fields: SchemaFieldConfig[];
39
- descend: () => R;
40
- }
41
-
42
- /**
43
- * Order by state
44
- */
45
- export interface OrderBy {
46
- stack: VisitStack[];
47
- asc: boolean;
48
- }
49
-
50
- /**
51
- * Visitation instance
52
- */
53
- export interface VisitInstanceNode<R> extends VisitNode<R> {
54
- value: unknown;
55
- }
56
-
57
- /**
58
- * Visit handler
59
- */
60
- export interface VisitHandler<R, U extends VisitNode<R> = VisitNode<R>> {
61
- onRoot(config: U & { config: SchemaClassConfig }): R;
62
- onSub(config: U & { config: SchemaFieldConfig }): R;
63
- onSimple(config: Omit<U, 'descend'> & { config: SchemaFieldConfig }): R;
64
- }
@@ -1,177 +0,0 @@
1
- import { type AsyncContext, WithAsyncContext } from '@travetto/context';
2
- import { ModelRegistryIndex } from '@travetto/model';
3
- import type { Class } from '@travetto/runtime';
4
- import { type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
5
-
6
- import type { Connection } from './connection/base.ts';
7
- import { Connected, Transactional } from './connection/decorator.ts';
8
- import type { SQLDialect } from './dialect/base.ts';
9
- import type { VisitStack } from './types.ts';
10
- import { SQLModelUtil } from './util.ts';
11
-
12
- type UpsertStructure = { dropIndex: string[]; createIndex: string[]; table: string[] };
13
- const isSimpleField = (input: VisitStack | undefined): input is SchemaFieldConfig =>
14
- !!input && (!('type' in input) || (input.type && !SchemaRegistryIndex.has(input.type)));
15
-
16
- /**
17
- * Manage creation/updating of all tables
18
- */
19
- export class TableManager {
20
- #dialect: SQLDialect;
21
- context: AsyncContext;
22
-
23
- constructor(context: AsyncContext, dialect: SQLDialect) {
24
- this.#dialect = dialect;
25
- this.context = context;
26
- }
27
-
28
- #exec<T = unknown>(sql: string): Promise<{ records: T[]; count: number }> {
29
- return this.#dialect.executeSQL<T>(sql);
30
- }
31
-
32
- /**
33
- * Get a valid connection
34
- */
35
- get connection(): Connection {
36
- return this.#dialect.connection;
37
- }
38
-
39
- /**
40
- * Create all needed tables for a given class
41
- */
42
- async exportTables(cls: Class): Promise<string[]> {
43
- const out: string[] = [];
44
- for (const command of this.#dialect.getCreateAllTablesSQL(cls)) {
45
- out.push(command);
46
- }
47
- const indices = ModelRegistryIndex.getIndices(cls);
48
- if (indices) {
49
- for (const command of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
50
- out.push(command);
51
- }
52
- }
53
- return out;
54
- }
55
-
56
- @WithAsyncContext()
57
- @Connected()
58
- async getUpsertTablesSQL(cls: Class): Promise<UpsertStructure> {
59
- const sqlCommands: UpsertStructure = { dropIndex: [], createIndex: [], table: [] };
60
-
61
- const onVisit = async (type: Class, fields: SchemaFieldConfig[], path: VisitStack[]): Promise<void> => {
62
- const found = await this.#dialect.describeTable(this.#dialect.namespace(path));
63
- const existingFields = new Map(found?.columns.map(column => [column.name, column]) ?? []);
64
- const existingIndices = new Map(found?.indices.map(index => [index.name, index]) ?? []);
65
- const model = path.length === 1 ? ModelRegistryIndex.getConfig(type) : undefined;
66
- const indices = model ? ModelRegistryIndex.getIndices(type) : undefined;
67
- const requestedIndices = new Map((indices ?? []).map(index => [this.#dialect.getIndexName(type, index), index]) ?? []);
68
-
69
- // Manage fields
70
- if (!existingFields.size) {
71
- sqlCommands.table.push(this.#dialect.getCreateTableSQL(path));
72
- } else {
73
- // Existing
74
- // Fields
75
- const requestedFields = new Map(fields.map(field => [field.name, field]));
76
- const top = path.at(-1);
77
-
78
- if (isSimpleField(top)) {
79
- requestedFields.set(top.name, top);
80
- }
81
-
82
- for (const [column, field] of requestedFields.entries()) {
83
- if (!existingFields.has(column)) {
84
- sqlCommands.table.push(this.#dialect.getAddColumnSQL([...path, field]));
85
- } else if (this.#dialect.isColumnChanged(field, existingFields.get(column)!)) {
86
- sqlCommands.table.push(this.#dialect.getModifyColumnSQL([...path, field]));
87
- }
88
- }
89
-
90
- // TODO: Handle dropping tables that are FK'd when no longer in use
91
-
92
- for (const column of existingFields.keys()) {
93
- if (!requestedFields.has(column)) {
94
- sqlCommands.table.push(this.#dialect.getDropColumnSQL([...path, { name: column, type: undefined!, array: false }]));
95
- }
96
- }
97
- }
98
-
99
- // Manage indices
100
- for (const index of requestedIndices.keys()) {
101
- if (!existingIndices.has(index)) {
102
- const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
103
- if (sql) {
104
- sqlCommands.createIndex.push(sql);
105
- }
106
- } else if (this.#dialect.isIndexChanged(requestedIndices.get(index)!, existingIndices.get(index)!)) {
107
- sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
108
- const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
109
- if (sql) {
110
- sqlCommands.createIndex.push(sql);
111
- }
112
- }
113
- }
114
-
115
- for (const index of existingIndices.keys()) {
116
- if (!requestedIndices.has(index)) {
117
- sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
118
- }
119
- }
120
- };
121
-
122
- const schema = SchemaRegistryIndex.getConfig(cls);
123
- await SQLModelUtil.visitSchema(schema, {
124
- onRoot: async ({ config, path, fields, descend }) => {
125
- await onVisit(config.class, fields, path);
126
- return descend();
127
- },
128
- onSub: async ({ config, path, fields, descend }) => {
129
- await onVisit(config.type, fields, path);
130
- return descend();
131
- },
132
- onSimple: async ({ config, path, fields }) => {
133
- await onVisit(config.type, fields, path);
134
- }
135
- });
136
- return sqlCommands;
137
- }
138
-
139
- @WithAsyncContext()
140
- @Connected()
141
- @Transactional()
142
- async upsertTables(cls: Class): Promise<void> {
143
- // Enforce id length
144
- this.#dialect.enforceIdLength(cls);
145
-
146
- const sqlCommands = await this.getUpsertTablesSQL(cls);
147
- for (const key of ['dropIndex', 'table', 'createIndex'] as const) {
148
- for (const command of sqlCommands[key]) {
149
- await this.#exec(command);
150
- }
151
- }
152
- }
153
-
154
- /**
155
- * Drop all tables for a given class
156
- */
157
- @WithAsyncContext()
158
- @Connected()
159
- @Transactional()
160
- async dropTables(cls: Class): Promise<void> {
161
- for (const command of this.#dialect.getDropAllTablesSQL(cls)) {
162
- await this.#exec(command);
163
- }
164
- }
165
-
166
- /**
167
- * Drop all tables for a given class
168
- */
169
- @WithAsyncContext()
170
- @Connected()
171
- @Transactional()
172
- async truncateTables(cls: Class): Promise<void> {
173
- for (const command of this.#dialect.getTruncateAllTablesSQL(cls)) {
174
- await this.#exec(command);
175
- }
176
- }
177
- }
package/src/util.ts DELETED
@@ -1,352 +0,0 @@
1
- import { ModelRegistryIndex, type ModelType, type OptionalId } from '@travetto/model';
2
- import type { SelectClause, SortClause } from '@travetto/model-query';
3
- import { type Class, castKey, castTo, TypedObject } from '@travetto/runtime';
4
- import { DataUtil, type SchemaClassConfig, type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
5
-
6
- import type { DialectState, InsertWrapper, OrderBy, VisitHandler, VisitInstanceNode, VisitState } from './internal/types.ts';
7
- import { TableSymbol, type VisitStack } from './types.ts';
8
-
9
- type FieldCacheEntry = {
10
- local: SchemaFieldConfig[];
11
- localMap: Record<string, SchemaFieldConfig>;
12
- foreign: SchemaFieldConfig[];
13
- foreignMap: Record<string, SchemaFieldConfig>;
14
- };
15
-
16
- /**
17
- * Utilities for dealing with SQL operations
18
- */
19
- export class SQLModelUtil {
20
- static #schemaFieldsCache = new Map<Class, FieldCacheEntry>();
21
-
22
- /**
23
- * Creates a new visitation stack with the class as the root
24
- */
25
- static classToStack(type: Class): VisitStack[] {
26
- return [{ type, name: type.name }];
27
- }
28
-
29
- /**
30
- * Clean results from db, by dropping internal fields
31
- */
32
- static cleanResults<T, U = T>(state: DialectState, item: T[]): U[];
33
- static cleanResults<T, U = T>(state: DialectState, item: T): U;
34
- static cleanResults<T, U = T>(state: DialectState, item: T | T[]): U | U[] {
35
- if (Array.isArray(item)) {
36
- return item.filter(value => value !== null && value !== undefined).map(value => this.cleanResults(state, value));
37
- } else if (!DataUtil.isSimpleValue(item)) {
38
- for (const key of TypedObject.keys(item)) {
39
- if (
40
- item[key] === null ||
41
- item[key] === undefined ||
42
- key === state.parentPathField.name ||
43
- key === state.pathField.name ||
44
- key === state.idxField.name
45
- ) {
46
- delete item[key];
47
- } else {
48
- item[key] = this.cleanResults(state, item[key]);
49
- }
50
- }
51
- return castTo({ ...item });
52
- } else {
53
- return castTo(item);
54
- }
55
- }
56
-
57
- /**
58
- * Get all available fields at current stack path
59
- */
60
- static getFieldsByLocation(stack: VisitStack[]): FieldCacheEntry {
61
- const top = stack.at(-1)!;
62
- const config = SchemaRegistryIndex.getOptional(top.type)?.get();
63
-
64
- if (config && this.#schemaFieldsCache.has(config.class)) {
65
- return this.#schemaFieldsCache.get(config.class)!;
66
- }
67
-
68
- if (!config) {
69
- // If a simple type, it is it's own field
70
- const field: SchemaFieldConfig = castTo({ ...top });
71
- return {
72
- local: [field],
73
- localMap: { [field.name]: field },
74
- foreign: [],
75
- foreignMap: {}
76
- };
77
- }
78
-
79
- const hasModel = ModelRegistryIndex.has(config.class)!;
80
- const fields = Object.values(config.fields).map(field => ({ ...field }));
81
-
82
- // Polymorphic
83
- if (hasModel && config.discriminatedBase) {
84
- const fieldMap = new Set(fields.map(field => field.name));
85
- for (const type of SchemaRegistryIndex.getDiscriminatedClasses(config.class)) {
86
- const typeConfig = SchemaRegistryIndex.getConfig(type);
87
- for (const [fieldName, field] of Object.entries<SchemaFieldConfig>(typeConfig.fields)) {
88
- if (!fieldMap.has(fieldName)) {
89
- fieldMap.add(fieldName);
90
- fields.push({ ...field, required: { active: false } });
91
- }
92
- }
93
- }
94
- }
95
-
96
- const entry: FieldCacheEntry = {
97
- localMap: {},
98
- foreignMap: {},
99
- local: fields.filter(field => !SchemaRegistryIndex.has(field.type) && !field.array),
100
- foreign: fields.filter(field => SchemaRegistryIndex.has(field.type) || field.array)
101
- };
102
-
103
- entry.local.reduce((map, field) => (map[field.name] = field) && map, entry.localMap);
104
- entry.foreign.reduce((map, field) => (map[field.name] = field) && map, entry.foreignMap);
105
-
106
- this.#schemaFieldsCache.set(config.class, entry);
107
-
108
- return entry;
109
- }
110
-
111
- /**
112
- * Process a schema structure, synchronously
113
- */
114
- static visitSchemaSync(
115
- config: SchemaClassConfig | SchemaFieldConfig,
116
- handler: VisitHandler<void>,
117
- state: VisitState = { path: [] }
118
- ): void {
119
- const path = 'fields' in config ? this.classToStack(config.class) : [...state.path, config];
120
- const { local: fields, foreign } = this.getFieldsByLocation(path);
121
-
122
- const descend = (): void => {
123
- for (const field of foreign) {
124
- if (SchemaRegistryIndex.has(field.type)) {
125
- this.visitSchemaSync(field, handler, { path });
126
- } else {
127
- handler.onSimple({
128
- config: field,
129
- fields: [],
130
- path: [...path, field]
131
- });
132
- }
133
- }
134
- };
135
-
136
- if ('fields' in config) {
137
- handler.onRoot({ config, fields, descend, path });
138
- } else {
139
- handler.onSub({ config, fields, descend, path });
140
- }
141
- }
142
-
143
- /**
144
- * Visit a Schema structure
145
- */
146
- static async visitSchema(
147
- config: SchemaClassConfig | SchemaFieldConfig,
148
- handler: VisitHandler<Promise<void>>,
149
- state: VisitState = { path: [] }
150
- ): Promise<void> {
151
- const path = 'fields' in config ? this.classToStack(config.class) : [...state.path, config];
152
- const { local: fields, foreign } = this.getFieldsByLocation(path);
153
-
154
- const descend = async (): Promise<void> => {
155
- for (const field of foreign) {
156
- if (SchemaRegistryIndex.has(field.type)) {
157
- await this.visitSchema(field, handler, { path });
158
- } else {
159
- await handler.onSimple({
160
- config: field,
161
- fields: [],
162
- path: [...path, field]
163
- });
164
- }
165
- }
166
- };
167
-
168
- if ('fields' in config) {
169
- return handler.onRoot({ config, fields, descend, path });
170
- } else {
171
- return handler.onSub({ config, fields, descend, path });
172
- }
173
- }
174
-
175
- /**
176
- * Process a schema instance by visiting it synchronously. This is synchronous to prevent concurrent calls from breaking
177
- */
178
- static visitSchemaInstance<T extends ModelType>(
179
- cls: Class<T>,
180
- instance: T | OptionalId<T>,
181
- handler: VisitHandler<unknown, VisitInstanceNode<unknown>>
182
- ): void {
183
- const pathStack: unknown[] = [instance];
184
- this.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
185
- onRoot: config => {
186
- const { path } = config;
187
- path[0].name = instance.id!;
188
- handler.onRoot({ ...config, value: instance });
189
- return config.descend();
190
- },
191
- onSub: config => {
192
- const { config: field } = config;
193
- const topObject: Record<string, unknown> = castTo(pathStack.at(-1));
194
- const top = config.path.at(-1)!;
195
-
196
- if (field.name in topObject) {
197
- const valuesInput = topObject[field.name];
198
- const values = Array.isArray(valuesInput) ? valuesInput : [valuesInput];
199
-
200
- let i = 0;
201
- for (const value of values) {
202
- try {
203
- pathStack.push(value);
204
- config.path[config.path.length - 1] = { ...top, index: i++ };
205
- handler.onSub({ ...config, value });
206
- if (!field.array) {
207
- config.descend();
208
- }
209
- } finally {
210
- pathStack.pop();
211
- }
212
- i += 1;
213
- }
214
- if (field.array) {
215
- config.descend();
216
- }
217
- }
218
- },
219
- onSimple: config => {
220
- const { config: field } = config;
221
- const topObject: Record<string, unknown> = castTo(pathStack.at(-1));
222
- const value = topObject[field.name];
223
- return handler.onSimple({ ...config, value });
224
- }
225
- });
226
- }
227
-
228
- /**
229
- * Get list of selected fields
230
- */
231
- static select<T>(cls: Class<T>, select?: SelectClause<T>): SchemaFieldConfig[] {
232
- if (!select || Object.keys(select).length === 0) {
233
- return [{ type: cls, name: '*', class: cls, array: false }];
234
- }
235
-
236
- const { localMap } = this.getFieldsByLocation(this.classToStack(cls));
237
-
238
- let toGet = new Set<string>();
239
-
240
- for (const [key, value] of TypedObject.entries(select)) {
241
- if (typeof key === 'string' && !DataUtil.isPlainObject(select[key]) && localMap[key]) {
242
- if (!value) {
243
- if (toGet.size === 0) {
244
- toGet = new Set(Object.keys(SchemaRegistryIndex.getConfig(cls).fields));
245
- }
246
- toGet.delete(key);
247
- } else {
248
- toGet.add(key);
249
- }
250
- }
251
- }
252
- return [...toGet].map(field => localMap[field]);
253
- }
254
-
255
- /**
256
- * Get list of Order By clauses
257
- */
258
- static orderBy<T>(cls: Class<T>, sort: SortClause<T>[]): OrderBy[] {
259
- return sort.map((cl: Record<string, unknown>) => {
260
- let schema: SchemaClassConfig = SchemaRegistryIndex.getConfig(cls);
261
- const stack = this.classToStack(cls);
262
- let found: OrderBy | undefined;
263
- while (!found) {
264
- const key = Object.keys(cl)[0];
265
- const value = cl[key];
266
- const field = { ...schema.fields[key] };
267
- if (DataUtil.isPrimitive(value)) {
268
- stack.push(field);
269
- found = { stack, asc: value === 1 };
270
- } else {
271
- stack.push(field);
272
- schema = SchemaRegistryIndex.getConfig(field.type);
273
- cl = castTo(value);
274
- }
275
- }
276
- return found;
277
- });
278
- }
279
-
280
- /**
281
- * Find all dependent fields via child tables
282
- */
283
- static collectDependents<T>(state: DialectState, parent: unknown, items: T[], field?: SchemaFieldConfig): Record<string, T> {
284
- if (field) {
285
- const isSimple = SchemaRegistryIndex.has(field.type);
286
- for (const item of items) {
287
- const parentKey: string = castTo(item[castKey<T>(state.parentPathField.name)]);
288
- const root = castTo<Record<string, Record<string, unknown>>>(parent)[parentKey];
289
- const fieldKey = castKey<typeof root | T>(field.name);
290
- if (field.array) {
291
- if (!root[fieldKey]) {
292
- root[fieldKey] = [isSimple ? item : item[fieldKey]];
293
- } else if (Array.isArray(root[fieldKey])) {
294
- root[fieldKey].push(isSimple ? item : item[fieldKey]);
295
- }
296
- } else {
297
- root[fieldKey] = isSimple ? item : item[fieldKey];
298
- }
299
- }
300
- }
301
-
302
- const mapping: Record<string, T> = {};
303
- for (const item of items) {
304
- const key = item[castKey<T>(state.pathField.name)];
305
- if (typeof key === 'string') {
306
- mapping[key] = item;
307
- }
308
- }
309
- return mapping;
310
- }
311
-
312
- /**
313
- * Build table name via stack path
314
- */
315
- static buildTable(list: VisitStack[]): string {
316
- const top = list.at(-1)!;
317
- if (!top[TableSymbol]) {
318
- top[TableSymbol] = list.map((item, i) => (i === 0 ? ModelRegistryIndex.getStoreName(item.type) : item.name)).join('_');
319
- }
320
- return top[TableSymbol]!;
321
- }
322
-
323
- /**
324
- * Build property path for a table/field given the current stack
325
- */
326
- static buildPath(list: VisitStack[]): string {
327
- return list.map(item => `${item.name}${item.index ? `[${item.index}]` : ''}`).join('.');
328
- }
329
-
330
- /**
331
- * Get insert statements for a given class, and its child tables
332
- */
333
- static async getInserts<T extends ModelType>(cls: Class<T>, items: (T | OptionalId<T>)[]): Promise<InsertWrapper[]> {
334
- const wrappers: Record<string, InsertWrapper> = {};
335
-
336
- const track = (stack: VisitStack[], value: unknown): void => {
337
- const key = this.buildTable(stack);
338
- (wrappers[key] ??= { stack, records: [] }).records.push({ stack, value });
339
- };
340
-
341
- items.map(item =>
342
- this.visitSchemaInstance(cls, item, {
343
- onRoot: ({ path, value }) => track(path, value),
344
- onSub: ({ path, value }) => track(path, value),
345
- onSimple: ({ path, value }) => track(path, value)
346
- })
347
- );
348
-
349
- const result = [...Object.values(wrappers)].toSorted((a, b) => a.stack.length - b.stack.length);
350
- return result;
351
- }
352
- }