@travetto/model-sql 8.0.0-alpha.3 → 8.0.0-alpha.30

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