@travetto/model-sql 7.0.0-rc.1 → 7.0.0-rc.3

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/src/service.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ModelType,
3
- BulkOp, BulkResponse, ModelCrudSupport, ModelStorageSupport, ModelBulkSupport,
3
+ BulkOperation, BulkResponse, ModelCrudSupport, ModelStorageSupport, ModelBulkSupport,
4
4
  NotFoundError, ModelRegistryIndex, ExistsError, OptionalId, ModelIdSource,
5
5
  ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
6
6
  } from '@travetto/model';
7
7
  import { castTo, Class } from '@travetto/runtime';
8
- import { DataUtil, SchemaChange } from '@travetto/schema';
8
+ import { DataUtil } from '@travetto/schema';
9
9
  import { AsyncContext } from '@travetto/context';
10
10
  import { Injectable } from '@travetto/di';
11
11
  import {
@@ -72,11 +72,11 @@ export class SQLModelService implements
72
72
  )
73
73
  )).records : [];
74
74
 
75
- const allIds = new Set(all.map(el => el.id));
75
+ const allIds = new Set(all.map(type => type.id));
76
76
 
77
- for (const [el, idx] of toCheck.entries()) {
78
- if (!allIds.has(el)) { // If not found
79
- addedIds.set(idx, el);
77
+ for (const [id, idx] of toCheck.entries()) {
78
+ if (!allIds.has(id)) { // If not found
79
+ addedIds.set(idx, id);
80
80
  }
81
81
  }
82
82
 
@@ -99,31 +99,23 @@ export class SQLModelService implements
99
99
  }
100
100
 
101
101
  async postConstruct(): Promise<void> {
102
- if (this.#dialect) {
103
- if (this.#dialect.conn.init) {
104
- await this.#dialect.conn.init();
105
- }
106
- this.idSource = ModelCrudUtil.uuidSource(this.#dialect.ID_LEN);
107
- this.#manager = new TableManager(this.#context, this.#dialect);
108
- await ModelStorageUtil.registerModelChangeListener(this);
109
- ModelExpiryUtil.registerCull(this);
110
- }
111
- }
112
-
113
- get conn(): Connection {
114
- return this.#dialect.conn;
102
+ await this.#dialect.connection.init?.();
103
+ this.idSource = ModelCrudUtil.uuidSource(this.#dialect.ID_LENGTH);
104
+ this.#manager = new TableManager(this.#context, this.#dialect);
105
+ await ModelStorageUtil.storageInitialization(this);
106
+ ModelExpiryUtil.registerCull(this);
115
107
  }
116
108
 
117
- async exportModel<T extends ModelType>(e: Class<T>): Promise<string> {
118
- return (await this.#manager.exportTables(e)).join('\n');
109
+ get connection(): Connection {
110
+ return this.#dialect.connection;
119
111
  }
120
112
 
121
- async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
122
- await this.#manager.changeSchema(cls, change);
113
+ async exportModel<T extends ModelType>(cls: Class<T>): Promise<string> {
114
+ return (await this.#manager.exportTables(cls)).join('\n');
123
115
  }
124
116
 
125
- async createModel(cls: Class): Promise<void> {
126
- await this.#manager.createTables(cls);
117
+ async upsertModel(cls: Class): Promise<void> {
118
+ await this.#manager.upsertTables(cls);
127
119
  }
128
120
 
129
121
  async deleteModel(cls: Class): Promise<void> {
@@ -144,11 +136,11 @@ export class SQLModelService implements
144
136
  for (const ins of this.#dialect.getAllInsertSQL(cls, prepped)) {
145
137
  await this.#exec(ins);
146
138
  }
147
- } catch (err) {
148
- if (err instanceof ExistsError) {
139
+ } catch (error) {
140
+ if (error instanceof ExistsError) {
149
141
  throw new ExistsError(cls, prepped.id);
150
142
  } else {
151
- throw err;
143
+ throw error;
152
144
  }
153
145
  }
154
146
  return prepped;
@@ -166,9 +158,9 @@ export class SQLModelService implements
166
158
  if (item.id) {
167
159
  await this.#deleteRaw(cls, item.id, {}, false);
168
160
  }
169
- } catch (err) {
170
- if (!(err instanceof NotFoundError)) {
171
- throw err;
161
+ } catch (error) {
162
+ if (!(error instanceof NotFoundError)) {
163
+ throw error;
172
164
  }
173
165
  }
174
166
  return await this.create(cls, item);
@@ -203,7 +195,7 @@ export class SQLModelService implements
203
195
  }
204
196
 
205
197
  @Transactional()
206
- async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOp<T>[]): Promise<BulkResponse> {
198
+ async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse> {
207
199
 
208
200
  const { insertedIds, upsertedIds, existingUpsertedIds } = await ModelBulkUtil.preStore(cls, operations, this);
209
201
 
@@ -211,16 +203,17 @@ export class SQLModelService implements
211
203
 
212
204
  await this.#checkUpsertedIds(cls,
213
205
  addedIds,
214
- new Map([...existingUpsertedIds.entries()].map(([k, v]) => [v, k]))
206
+ new Map([...existingUpsertedIds.entries()].map(([key, value]) => [value, key]))
215
207
  );
216
208
 
217
- const get = <K extends keyof BulkOp<T>>(k: K): Required<BulkOp<T>>[K][] =>
218
- operations.map(x => x[k]).filter((x): x is Required<BulkOp<T>>[K] => !!x);
209
+ const get = <K extends keyof BulkOperation<T>>(key: K): Required<BulkOperation<T>>[K][] =>
210
+ operations.map(item => item[key]).filter((item): item is Required<BulkOperation<T>>[K] => !!item);
219
211
 
220
- const getStatements = async (k: keyof BulkOp<T>): Promise<InsertWrapper[]> =>
221
- (await SQLModelUtil.getInserts(cls, get(k))).filter(x => !!x.records.length);
212
+ const getStatements = async (key: keyof BulkOperation<T>): Promise<InsertWrapper[]> =>
213
+ (await SQLModelUtil.getInserts(cls, get(key))).filter(wrapper => !!wrapper.records.length);
222
214
 
223
- const deletes = [{ stack: SQLModelUtil.classToStack(cls), ids: get('delete').map(x => x.id) }].filter(x => !!x.ids.length);
215
+ const deletes = [{ stack: SQLModelUtil.classToStack(cls), ids: get('delete').map(wrapper => wrapper.id) }]
216
+ .filter(wrapper => !!wrapper.ids.length);
224
217
 
225
218
  const [inserts, upserts, updates] = await Promise.all([
226
219
  getStatements('insert'),
@@ -242,19 +235,19 @@ export class SQLModelService implements
242
235
  @Connected()
243
236
  async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
244
237
  await QueryVerifier.verify(cls, query);
245
- const { records: res } = await this.#exec<T>(this.#dialect.getQuerySQL(cls, query, ModelQueryUtil.getWhereClause(cls, query.where)));
238
+ const { records } = await this.#exec<T>(this.#dialect.getQuerySQL(cls, query, ModelQueryUtil.getWhereClause(cls, query.where)));
246
239
  if (ModelRegistryIndex.has(cls)) {
247
- await this.#dialect.fetchDependents(cls, res, query && query.select);
240
+ await this.#dialect.fetchDependents(cls, records, query && query.select);
248
241
  }
249
242
 
250
- const cleaned = SQLModelUtil.cleanResults<T>(this.#dialect, res);
251
- return await Promise.all(cleaned.map(m => ModelCrudUtil.load(cls, m)));
243
+ const cleaned = SQLModelUtil.cleanResults<T>(this.#dialect, records);
244
+ return await Promise.all(cleaned.map(item => ModelCrudUtil.load(cls, item)));
252
245
  }
253
246
 
254
247
  @Connected()
255
248
  async queryOne<T extends ModelType>(cls: Class<T>, builder: ModelQuery<T>, failOnMany = true): Promise<T> {
256
- const res = await this.query<T>(cls, { ...builder, limit: failOnMany ? 2 : 1 });
257
- return ModelQueryUtil.verifyGetSingleCounts<T>(cls, failOnMany, res, builder.where);
249
+ const results = await this.query<T>(cls, { ...builder, limit: failOnMany ? 2 : 1 });
250
+ return ModelQueryUtil.verifyGetSingleCounts<T>(cls, failOnMany, results, builder.where);
258
251
  }
259
252
 
260
253
  @Connected()
@@ -294,43 +287,43 @@ export class SQLModelService implements
294
287
  @Connected()
295
288
  async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
296
289
  await QueryVerifier.verify(cls, query);
297
- const q = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
298
- const results = await this.query<T>(cls, q);
290
+ const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
291
+ const results = await this.query<T>(cls, resolvedQuery);
299
292
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (a, b) => b, query?.limit);
300
293
  }
301
294
 
302
295
  @Connected()
303
296
  async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
304
297
  await QueryVerifier.verify(cls, query);
305
- const q = ModelQuerySuggestUtil.getSuggestFieldQuery(cls, field, prefix, query);
306
- const results = await this.query(cls, q);
298
+ const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery(cls, field, prefix, query);
299
+ const results = await this.query(cls, resolvedQuery);
307
300
 
308
301
  const modelTypeField: ValidStringFields<ModelType> = castTo(field);
309
- return ModelQuerySuggestUtil.combineSuggestResults(cls, modelTypeField, prefix, results, x => x, query?.limit);
302
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, modelTypeField, prefix, results, result => result, query?.limit);
310
303
  }
311
304
 
312
305
  @Connected()
313
306
  async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
314
307
  await QueryVerifier.verify(cls, query);
315
- const col = this.#dialect.ident(field);
316
- const ttl = this.#dialect.ident('count');
317
- const key = this.#dialect.ident('key');
318
- const q = [
308
+ const col = this.#dialect.identifier(field);
309
+ const ttl = this.#dialect.identifier('count');
310
+ const key = this.#dialect.identifier('key');
311
+ const sql = [
319
312
  `SELECT ${col} as ${key}, COUNT(${col}) as ${ttl}`,
320
313
  this.#dialect.getFromSQL(cls),
321
314
  ];
322
- q.push(
315
+ sql.push(
323
316
  this.#dialect.getWhereSQL(cls, ModelQueryUtil.getWhereClause(cls, query?.where))
324
317
  );
325
- q.push(
318
+ sql.push(
326
319
  `GROUP BY ${col}`,
327
320
  `ORDER BY ${ttl} DESC`
328
321
  );
329
322
 
330
- const results = await this.#exec<{ key: string, count: number }>(q.join('\n'));
331
- return results.records.map(x => {
332
- x.count = DataUtil.coerceType(x.count, Number);
333
- return x;
323
+ const results = await this.#exec<{ key: string, count: number }>(sql.join('\n'));
324
+ return results.records.map(result => {
325
+ result.count = DataUtil.coerceType(result.count, Number);
326
+ return result;
334
327
  });
335
328
  }
336
329
  }
@@ -1,8 +1,7 @@
1
1
  import { AsyncContext, WithAsyncContext } from '@travetto/context';
2
2
  import { ModelRegistryIndex } from '@travetto/model';
3
3
  import { Class } from '@travetto/runtime';
4
- import { ChangeEvent } from '@travetto/registry';
5
- import { SchemaChange } from '@travetto/schema';
4
+ import { SchemaRegistryIndex, type SchemaFieldConfig } from '@travetto/schema';
6
5
 
7
6
  import { Connected, Transactional } from './connection/decorator.ts';
8
7
  import { SQLDialect } from './dialect/base.ts';
@@ -10,6 +9,10 @@ import { SQLModelUtil } from './util.ts';
10
9
  import { Connection } from './connection/base.ts';
11
10
  import { VisitStack } from './types.ts';
12
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
+
13
16
  /**
14
17
  * Manage creation/updating of all tables
15
18
  */
@@ -27,59 +30,108 @@ export class TableManager {
27
30
  return this.#dialect.executeSQL<T>(sql);
28
31
  }
29
32
 
33
+ /**
34
+ * Get a valid connection
35
+ */
36
+ get connection(): Connection {
37
+ return this.#dialect.connection;
38
+ }
39
+
30
40
  /**
31
41
  * Create all needed tables for a given class
32
42
  */
33
43
  async exportTables(cls: Class): Promise<string[]> {
34
44
  const out: string[] = [];
35
- for (const op of this.#dialect.getCreateAllTablesSQL(cls)) {
36
- out.push(op);
45
+ for (const command of this.#dialect.getCreateAllTablesSQL(cls)) {
46
+ out.push(command);
37
47
  }
38
48
  const indices = ModelRegistryIndex.getConfig(cls).indices;
39
49
  if (indices) {
40
- for (const op of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
41
- out.push(op);
50
+ for (const command of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
51
+ out.push(command);
42
52
  }
43
53
  }
44
54
  return out;
45
55
  }
46
56
 
47
- /**
48
- * Create all needed tables for a given class
49
- */
50
57
  @WithAsyncContext()
51
58
  @Connected()
52
- @Transactional()
53
- async createTables(cls: Class): Promise<void> {
54
- for (const op of this.#dialect.getCreateAllTablesSQL(cls)) {
55
- await this.#exec(op);
56
- }
57
- const indices = ModelRegistryIndex.getConfig(cls).indices;
58
- if (indices) {
59
- for (const op of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
60
- try {
61
- await this.#exec(op);
62
- } catch (err) {
63
- if (!(err instanceof Error)) {
64
- throw err;
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
+
70
+ // Manage fields
71
+ if (!existingFields.size) {
72
+ sqlCommands.table.push(this.#dialect.getCreateTableSQL(path));
73
+ } else { // 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]));
65
87
  }
66
- if (!/\bexists|duplicate\b/i.test(err.message)) {
67
- throw err;
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 }]));
68
95
  }
69
96
  }
70
97
  }
71
- }
98
+
99
+ // Manage indices
100
+ for (const index of requestedIndices.keys()) {
101
+ if (!existingIndices.has(index)) {
102
+ sqlCommands.createIndex.push(this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!));
103
+ } else if (this.#dialect.isIndexChanged(requestedIndices.get(index)!, existingIndices.get(index)!)) {
104
+ sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
105
+ sqlCommands.createIndex.push(this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!));
106
+ }
107
+ }
108
+
109
+ for (const index of existingIndices.keys()) {
110
+ if (!requestedIndices.has(index)) {
111
+ sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
112
+ }
113
+ }
114
+ };
115
+
116
+ const schema = SchemaRegistryIndex.getConfig(cls);
117
+ await SQLModelUtil.visitSchema(schema, {
118
+ onRoot: async ({ config, path, fields, descend }) => { await onVisit(config.class, fields, path); return descend(); },
119
+ onSub: async ({ config, path, fields, descend }) => { await onVisit(config.type, fields, path); return descend(); },
120
+ onSimple: async ({ config, path, fields }) => { await onVisit(config.type, fields, path); }
121
+ });
122
+ return sqlCommands;
72
123
  }
73
124
 
74
- /**
75
- * Drop all tables for a given class
76
- */
77
125
  @WithAsyncContext()
78
126
  @Connected()
79
127
  @Transactional()
80
- async dropTables(cls: Class): Promise<void> {
81
- for (const op of this.#dialect.getDropAllTablesSQL(cls)) {
82
- await this.#exec(op);
128
+ async upsertTables(cls: Class): Promise<void> {
129
+ // Enforce id length
130
+ this.#dialect.enforceIdLength(cls);
131
+
132
+ const sqlCommands = await this.getUpsertTablesSQL(cls);
133
+ for (const key of ['dropIndex', 'table', 'createIndex'] as const) {
134
+ await Promise.all(sqlCommands[key].map(command => this.#exec(command)));
83
135
  }
84
136
  }
85
137
 
@@ -89,43 +141,21 @@ export class TableManager {
89
141
  @WithAsyncContext()
90
142
  @Connected()
91
143
  @Transactional()
92
- async truncateTables(cls: Class): Promise<void> {
93
- for (const op of this.#dialect.getTruncateAllTablesSQL(cls)) {
94
- await this.#exec(op);
144
+ async dropTables(cls: Class): Promise<void> {
145
+ for (const command of this.#dialect.getDropAllTablesSQL(cls)) {
146
+ await this.#exec(command);
95
147
  }
96
148
  }
97
149
 
98
150
  /**
99
- * Get a valid connection
100
- */
101
- get conn(): Connection {
102
- return this.#dialect.conn;
103
- }
104
-
105
- /**
106
- * When the schema changes, update SQL
151
+ * Drop all tables for a given class
107
152
  */
108
153
  @WithAsyncContext()
109
- @Transactional()
110
154
  @Connected()
111
- async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
112
- try {
113
- const rootStack = SQLModelUtil.classToStack(cls);
114
-
115
- const changes = change.subs.reduce<Record<ChangeEvent<unknown>['type'], VisitStack[][]>>((acc, v) => {
116
- const path = v.path.map(f => ({ ...f }));
117
- for (const ev of v.fields) {
118
- acc[ev.type].push([...rootStack, ...path, { ...(ev.type === 'removing' ? ev.prev : ev.curr)! }]);
119
- }
120
- return acc;
121
- }, { added: [], changed: [], removing: [] });
122
-
123
- await Promise.all(changes.added.map(v => this.#dialect.executeSQL(this.#dialect.getAddColumnSQL(v))));
124
- await Promise.all(changes.changed.map(v => this.#dialect.executeSQL(this.#dialect.getModifyColumnSQL(v))));
125
- await Promise.all(changes.removing.map(v => this.#dialect.executeSQL(this.#dialect.getDropColumnSQL(v))));
126
- } catch (err) {
127
- // Failed to change
128
- console.error('Unable to change field', { error: err });
155
+ @Transactional()
156
+ async truncateTables(cls: Class): Promise<void> {
157
+ for (const command of this.#dialect.getTruncateAllTablesSQL(cls)) {
158
+ await this.#exec(command);
129
159
  }
130
160
  }
131
161
  }
package/src/types.ts CHANGED
@@ -6,6 +6,6 @@ export type VisitStack = {
6
6
  [TableSymbol]?: string;
7
7
  array?: boolean;
8
8
  type: Class;
9
- name: string | symbol;
9
+ name: string;
10
10
  index?: number;
11
11
  };