@travetto/model-sql 8.0.0-alpha.1 → 8.0.0-alpha.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-sql",
3
- "version": "8.0.0-alpha.1",
3
+ "version": "8.0.0-alpha.11",
4
4
  "type": "module",
5
5
  "description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
6
6
  "keywords": [
@@ -28,14 +28,15 @@
28
28
  "directory": "module/model-sql"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/config": "^8.0.0-alpha.1",
32
- "@travetto/context": "^8.0.0-alpha.1",
33
- "@travetto/model": "^8.0.0-alpha.1",
34
- "@travetto/model-query": "^8.0.0-alpha.1"
31
+ "@travetto/config": "^8.0.0-alpha.10",
32
+ "@travetto/context": "^8.0.0-alpha.10",
33
+ "@travetto/model": "^8.0.0-alpha.10",
34
+ "@travetto/model-indexed": "^8.0.0-alpha.11",
35
+ "@travetto/model-query": "^8.0.0-alpha.10"
35
36
  },
36
37
  "peerDependencies": {
37
- "@travetto/cli": "^8.0.0-alpha.1",
38
- "@travetto/test": "^8.0.0-alpha.1"
38
+ "@travetto/cli": "^8.0.0-alpha.15",
39
+ "@travetto/test": "^8.0.0-alpha.10"
39
40
  },
40
41
  "peerDependenciesMeta": {
41
42
  "@travetto/cli": {
@@ -1,8 +1,9 @@
1
1
  /* eslint-disable @stylistic/indent */
2
2
  import { DataUtil, type SchemaFieldConfig, SchemaRegistryIndex, type Point } from '@travetto/schema';
3
3
  import { type Class, RuntimeError, TypedObject, TimeUtil, castTo, castKey, toConcrete, JSONUtil } from '@travetto/runtime';
4
- import { type SelectClause, type Query, type SortClause, type WhereClause, type RetainQueryPrimitiveFields, ModelQueryUtil } from '@travetto/model-query';
5
- import type { BulkResponse, IndexConfig, ModelType } from '@travetto/model';
4
+ import { type SelectClause, type Query, type SortClause, type WhereClause, type RetainQueryPrimitiveFields, ModelQueryUtil, isModelQueryIndex } from '@travetto/model-query';
5
+ import { IndexNotSupported, type BulkResponse, type IndexConfig, type ModelType } from '@travetto/model';
6
+ import { isModelIndexedIndex } from '@travetto/model-indexed';
6
7
 
7
8
  import { SQLModelUtil } from '../util.ts';
8
9
  import type { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types.ts';
@@ -717,14 +718,14 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
717
718
  /**
718
719
  * Get all create indices need for a given class
719
720
  */
720
- getCreateAllIndicesSQL<T extends ModelType>(cls: Class<T>, indices: IndexConfig<T>[]): string[] {
721
- return indices.map(idx => this.getCreateIndexSQL(cls, idx));
721
+ getCreateAllIndicesSQL<T extends ModelType>(cls: Class<T>, indices: IndexConfig[]): string[] {
722
+ return indices.map(idx => this.getCreateIndexSQL(cls, idx)).filter((sql): sql is string => !!sql);
722
723
  }
723
724
 
724
725
  /**
725
726
  * Get index name
726
727
  */
727
- getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<ModelType>): string {
728
+ getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string {
728
729
  const table = this.namespace(SQLModelUtil.classToStack(cls));
729
730
  return ['idx', table, idx.name.toLowerCase().replaceAll('-', '_')].join('_');
730
731
  }
@@ -732,26 +733,43 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
732
733
  /**
733
734
  * Get CREATE INDEX sql
734
735
  */
735
- getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T>): string {
736
+ getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string | undefined {
737
+ const constraint = this.getIndexName(cls, idx);
736
738
  const table = this.namespace(SQLModelUtil.classToStack(cls));
737
- const fields: [string, boolean][] = idx.fields.map(field => {
738
- const key = TypedObject.keys(field)[0];
739
- const value = field[key];
740
- if (DataUtil.isPlainObject(value)) {
741
- throw new Error('Unable to supported nested fields for indices');
739
+
740
+ if (isModelQueryIndex(idx)) {
741
+ const fields: [string, boolean][] = idx.fields.map(field => {
742
+ const key = TypedObject.keys(field)[0];
743
+ const value = field[key];
744
+ if (DataUtil.isPlainObject(value)) {
745
+ throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
746
+ }
747
+ return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
748
+ });
749
+ return `CREATE ${idx.type === 'query:unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
750
+ .map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
751
+ .join(', ')});`;
752
+ } else if (isModelIndexedIndex(idx)) {
753
+ if ([...idx.sortTemplate, ...idx.keyTemplate].find(field => field.path.length > 1)) {
754
+ console.debug('Nested fields are not supported in ModelIndexed indices SQL', { index: idx.name });
755
+ return;
742
756
  }
743
- return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
744
- });
745
- const constraint = this.getIndexName(cls, idx);
746
- return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
747
- .map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
748
- .join(', ')});`;
757
+ const fields = [...idx.sortTemplate, ...idx.keyTemplate]
758
+ .map(({ path, value }) => `${this.identifier(path.join('_'))} ${value === -1 ? 'DESC' : 'ASC'}`)
759
+ .join(', ');
760
+ switch (idx.type) {
761
+ case 'indexed:keyed': return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
762
+ case 'indexed:sorted': return `CREATE INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
763
+ }
764
+ } else {
765
+ throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
766
+ }
749
767
  }
750
768
 
751
769
  /**
752
770
  * Get DROP INDEX sql
753
771
  */
754
- getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string): string {
772
+ getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig | string): string {
755
773
  const constraint = typeof idx === 'string' ? idx : this.getIndexName(cls, idx);
756
774
  return `DROP INDEX ${this.identifier(constraint)} ;`;
757
775
  }
@@ -1074,18 +1092,25 @@ ${this.getWhereSQL(cls, where!)}`;
1074
1092
  /**
1075
1093
  * Determine if an index has changed
1076
1094
  */
1077
- isIndexChanged(requested: IndexConfig<ModelType>, existing: SQLTableDescription['indices'][number]): boolean {
1078
- let result =
1079
- (existing.is_unique && requested.type !== 'unique')
1080
- || requested.fields.length !== existing.columns.length;
1081
-
1082
- for (let i = 0; i < requested.fields.length && !result; i++) {
1083
- const [[key, value]] = Object.entries(requested.fields[i]);
1084
- const desc = value === -1;
1085
- result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
1086
- }
1095
+ isIndexChanged(requested: IndexConfig, existing: SQLTableDescription['indices'][number]): boolean {
1096
+ if (isModelQueryIndex(requested)) {
1097
+ let result =
1098
+ (existing.is_unique && requested.type !== 'query:unique')
1099
+ || requested.fields.length !== existing.columns.length;
1100
+
1101
+ for (let i = 0; i < requested.fields.length && !result; i++) {
1102
+ const [[key, value]] = Object.entries(requested.fields[i]);
1103
+ const desc = value === -1;
1104
+ result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
1105
+ }
1087
1106
 
1088
- return result;
1107
+ return result;
1108
+ } else if (isModelIndexedIndex(requested)) {
1109
+ // TODO: Fill this out
1110
+ return false;
1111
+ } else {
1112
+ throw new IndexNotSupported(requested.class, requested, 'Only indexed and query indices are supported in SQL');
1113
+ }
1089
1114
  }
1090
1115
 
1091
1116
  /**
package/src/service.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  type ModelType,
3
- type BulkOperation, type BulkResponse, type ModelCrudSupport, type ModelStorageSupport, type ModelBulkSupport,
4
- NotFoundError, ModelRegistryIndex, ExistsError, type OptionalId, type ModelIdSource,
5
- ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
3
+ type BulkOperation, type BulkResponse, type ModelCrudSupport, type ModelStorageSupport, type ModelBulkSupport, NotFoundError,
4
+ ModelRegistryIndex, ExistsError, type OptionalId, type ModelIdSource, ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
6
5
  } from '@travetto/model';
7
- import { castTo, type Class } from '@travetto/runtime';
6
+ import {
7
+ type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ListPageOptions, ModelIndexedUtil,
8
+ type SingleItemIndex, type SortedIndexSelection, type ListPageResult, type SortedIndex, type FullKeyedIndexBody,
9
+ type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex
10
+ } from '@travetto/model-indexed';
11
+ import { castTo, type Class, JSONUtil } from '@travetto/runtime';
8
12
  import { DataUtil } from '@travetto/schema';
9
13
  import type { AsyncContext } from '@travetto/context';
10
14
  import { Injectable, PostConstruct } from '@travetto/di';
@@ -33,6 +37,7 @@ export class SQLModelService implements
33
37
  ModelCrudSupport, ModelStorageSupport,
34
38
  ModelBulkSupport, ModelQuerySupport,
35
39
  ModelQueryCrudSupport, ModelQueryFacetSupport,
40
+ ModelIndexedSupport,
36
41
  ModelQuerySuggestSupport {
37
42
 
38
43
  #manager: TableManager;
@@ -326,4 +331,114 @@ export class SQLModelService implements
326
331
  return result;
327
332
  });
328
333
  }
334
+
335
+ // Indexed support
336
+ @Connected()
337
+ async getByIndex<
338
+ T extends ModelType,
339
+ K extends KeyedIndexSelection<T>,
340
+ S extends SortedIndexSelection<T>
341
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
342
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
343
+ const results = await this.query(cls, castTo({ where: computed.project({ sort: true, includeId: true }) }));
344
+ if (results.length !== 1) {
345
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
346
+ }
347
+ return results[0];
348
+ }
349
+
350
+ @Connected()
351
+ @Transactional()
352
+ async deleteByIndex<
353
+ T extends ModelType,
354
+ K extends KeyedIndexSelection<T>,
355
+ S extends SortedIndexSelection<T>
356
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
357
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
358
+ const count = await this.deleteByQuery(cls, castTo({ where: computed.project({ sort: true, includeId: true }) }));
359
+ if (count === 0) {
360
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
361
+ }
362
+ }
363
+
364
+ @Connected()
365
+ @Transactional()
366
+ upsertByIndex<
367
+ T extends ModelType,
368
+ K extends KeyedIndexSelection<T>,
369
+ S extends SortedIndexSelection<T>
370
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
371
+ return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
372
+ }
373
+
374
+ @Connected()
375
+ @Transactional()
376
+ updateByIndex<
377
+ T extends ModelType,
378
+ K extends KeyedIndexSelection<T>,
379
+ S extends SortedIndexSelection<T>
380
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
381
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
382
+ }
383
+
384
+ @Connected()
385
+ @Transactional()
386
+ async updatePartialByIndex<
387
+ T extends ModelType,
388
+ K extends KeyedIndexSelection<T>,
389
+ S extends SortedIndexSelection<T>
390
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
391
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
392
+ return this.update(cls, item);
393
+ }
394
+
395
+ @Connected()
396
+ async pageByIndex<
397
+ T extends ModelType,
398
+ K extends KeyedIndexSelection<T>,
399
+ S extends SortedIndexSelection<T>
400
+ >(
401
+ cls: Class<T>,
402
+ idx: SortedIndex<T, K, S>,
403
+ body: KeyedIndexBody<T, K>,
404
+ options?: ListPageOptions
405
+ ): Promise<ListPageResult<T>> {
406
+ const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
407
+ const limit = options?.limit ?? 100;
408
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
409
+
410
+ const items = await this.query(cls, castTo({
411
+ where: computed.project(),
412
+ sort: idx.sortTemplate.map(part => ({ [part.path.join('.')]: part.value })),
413
+ limit, offset
414
+ }));
415
+ return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
416
+ }
417
+
418
+ @ConnectedIterator()
419
+ async * listByIndex<
420
+ T extends ModelType,
421
+ K extends KeyedIndexSelection<T>,
422
+ S extends SortedIndexSelection<T>
423
+ >(
424
+ cls: Class<T>,
425
+ idx: SortedIndex<T, K, S>,
426
+ body: KeyedIndexBody<T, K>,
427
+ ): AsyncIterable<T> {
428
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
429
+ let offset = 0;
430
+ while (offset >= 0) {
431
+ const items = await this.query(cls, castTo({
432
+ where: computed.project(),
433
+ sort: idx.sortTemplate.map(part => ({ [part.path.join('.')]: part.value })),
434
+ limit: 100, offset
435
+ }));
436
+ if (items.length === 0) {
437
+ offset = -1;
438
+ } else {
439
+ offset += items.length;
440
+ yield* items;
441
+ }
442
+ }
443
+ }
329
444
  }
@@ -45,7 +45,7 @@ export class TableManager {
45
45
  for (const command of this.#dialect.getCreateAllTablesSQL(cls)) {
46
46
  out.push(command);
47
47
  }
48
- const indices = ModelRegistryIndex.getConfig(cls).indices;
48
+ const indices = ModelRegistryIndex.getIndices(cls);
49
49
  if (indices) {
50
50
  for (const command of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
51
51
  out.push(command);
@@ -64,7 +64,8 @@ export class TableManager {
64
64
  const existingFields = new Map(found?.columns.map(column => [column.name, column]) ?? []);
65
65
  const existingIndices = new Map(found?.indices.map(index => [index.name, index]) ?? []);
66
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]) ?? []);
67
+ const indices = model ? ModelRegistryIndex.getIndices(type) : undefined;
68
+ const requestedIndices = new Map((indices ?? []).map(index => [this.#dialect.getIndexName(type, index), index]) ?? []);
68
69
 
69
70
  // Manage fields
70
71
  if (!existingFields.size) {
@@ -98,10 +99,16 @@ export class TableManager {
98
99
  // Manage indices
99
100
  for (const index of requestedIndices.keys()) {
100
101
  if (!existingIndices.has(index)) {
101
- sqlCommands.createIndex.push(this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!));
102
+ const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
103
+ if (sql) {
104
+ sqlCommands.createIndex.push(sql);
105
+ }
102
106
  } else if (this.#dialect.isIndexChanged(requestedIndices.get(index)!, existingIndices.get(index)!)) {
103
107
  sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
104
- sqlCommands.createIndex.push(this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!));
108
+ const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
109
+ if (sql) {
110
+ sqlCommands.createIndex.push(sql);
111
+ }
105
112
  }
106
113
  }
107
114