@travetto/model-sql 8.0.0-alpha.9 → 8.0.1

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/README.md CHANGED
@@ -13,18 +13,38 @@ npm install @travetto/model-sql
13
13
  yarn add @travetto/model-sql
14
14
  ```
15
15
 
16
- The current SQL client support stands at:
17
- * [MySQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-mysql#readme "MySQL backing for the travetto model module, with real-time modeling support for SQL schemas.") - MySQL 8.6+
16
+ This module provides the core SQL foundation for [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") datastores. The current SQL client implementations include:
17
+ * [MySQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-mysql#readme "MySQL backing for the travetto model module, with real-time modeling support for SQL schemas.") - MySQL 8.0+
18
18
  * [PostgreSQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-postgres#readme "PostgreSQL backing for the travetto model module, with real-time modeling support for SQL schemas.") - Postgres 14+
19
- * [SQLite Model Service](https://github.com/travetto/travetto/tree/main/module/model-sqlite#readme "SQLite backing for the travetto model module, with real-time modeling support for SQL schemas.") - (bettersqlite 8.0+)
20
- * `SQL Server` - Currently unsupported
21
- * `Oracle` - Currently unsupported
19
+ * [SQLite Model Service](https://github.com/travetto/travetto/tree/main/module/model-sqlite#readme "SQLite backing for the travetto model module, with real-time modeling support for SQL schemas.") - SQLite (Node native `DatabaseSync`)
22
20
 
23
21
  **Note**: Wider client support will roll out as usage increases.
24
22
 
25
- ## Assumed Behavior
26
- The [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") works quite a bit different than the average [Object Relationship Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) in that it makes assertions about how data is stored in the database. The primary goal of the [SQL](https://en.wikipedia.org/wiki/SQL) support is not to handle every scenario that a relational database can provide, but to integrate with the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") structure, while leverage relational datastores to the best of their abilities.
23
+ ## Assumed Behavior & Schema Design
24
+ The [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") works quite a bit differently than the average [Object Relationship Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) in that it makes assertions about how data is stored in the database. The primary goal of the [SQL](https://en.wikipedia.org/wiki/SQL) support is not to handle every legacy relational database scenario, but to integrate with the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") structure while leveraging relational datastores to the best of their abilities.
27
25
 
28
- The primary difference is around unique identifiers, and how parent/child relationships are managed. In a normal database primary keys could be composite values between various fields. For example a unique identifier could be a combination of `date` + `orderNumber` + `customerNumber`. This is perfectly normal in a relational model, but [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") assumes unique identifiers ([UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)) as 32-character hexadecimal values. In addition to these unique values, the parent's identifier is required in all children values. This allows for some fairly optimized querying, updates, and deletions on changes.
26
+ The primary design maps each model class to a single table where simple fields (primitives, dates, enums) are mapped to individual columns, and complex fields (objects and arrays) are serialized and stored as native JSON document columns. Every table requires a primary key column (`id`), and indices are compiled directly to standard database columns or SQL functional expressions over JSON paths.
29
27
 
30
- What this translates to, is that the framework here dictates the final schema, and doesn't support adapting to existing relational data stores. In greenfield projects, this is not an issue, but will most likely preclude its use in adapting to existing relational data sets.
28
+ In development mode, storage modifications are applied dynamically in real time to match model definitions, minimizing manual database migrations.
29
+
30
+ ## Transactions
31
+ Transaction state is tracked seamlessly using [Async Context](https://github.com/travetto/travetto/tree/main/module/context#readme "Async-aware state management, maintaining context across asynchronous calls."). Methods can be wrapped using the [Transactional](https://github.com/travetto/travetto/tree/main/module/model-sql/src/connection.ts#L18) decorator to run operations within a managed transaction.
32
+
33
+ Supported transaction modes include:
34
+ * `required` - Joins an active transaction if one exists, or starts a new top-level transaction.
35
+ * `isolated` - Begins a SAVEPOINT / nested transaction isolated from the surrounding context.
36
+ * `force` - Always creates a separate SAVEPOINT for nested operations.
37
+
38
+ ## Bulk Operations
39
+ Bulk operations (`processBulk`) batch insert, delete, and update statements for high performance. Bulk updates utilize standard ANSI SQL `CASE ... WHEN` constructs to update multiple records in a single database query across all SQL engines.
40
+
41
+ ## Supported Features
42
+ All SQL model service implementations derive from [BaseSQLModelService](https://github.com/travetto/travetto/tree/main/module/model-sql/src/service.ts#L68) and support:
43
+ * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L60)
44
+ * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
45
+ * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
46
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
47
+ * [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
48
+ * [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
49
+ * [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/suggest.ts#L12)
50
+ * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
package/__index__.ts CHANGED
@@ -1,7 +1,5 @@
1
- export * from './src/config.ts';
1
+ export * from './src/connection.ts';
2
+ export * from './src/dialect.ts';
3
+ export * from './src/schema.ts';
2
4
  export * from './src/service.ts';
3
- export * from './src/connection/base.ts';
4
- export * from './src/connection/decorator.ts';
5
- export * from './src/dialect/base.ts';
6
- export * from './src/util.ts';
7
- export * from './src/types.ts';
5
+ export * from './src/types.ts';
package/package.json CHANGED
@@ -1,41 +1,45 @@
1
1
  {
2
2
  "name": "@travetto/model-sql",
3
- "version": "8.0.0-alpha.9",
4
- "type": "module",
3
+ "version": "8.0.1",
5
4
  "description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
6
5
  "keywords": [
7
- "sql",
8
6
  "data-modeling",
9
- "real-time",
10
7
  "model",
8
+ "real-time",
9
+ "sql",
11
10
  "travetto",
12
11
  "typescript"
13
12
  ],
14
13
  "homepage": "https://travetto.io",
15
14
  "license": "MIT",
16
15
  "author": {
17
- "email": "travetto.framework@gmail.com",
18
- "name": "Travetto Framework"
16
+ "name": "Travetto Framework",
17
+ "email": "travetto.framework@gmail.com"
18
+ },
19
+ "repository": {
20
+ "url": "git+https://github.com/travetto/travetto.git",
21
+ "directory": "module/model-sql"
19
22
  },
20
23
  "files": [
21
24
  "__index__.ts",
22
25
  "src",
23
26
  "support"
24
27
  ],
28
+ "type": "module",
25
29
  "main": "__index__.ts",
26
- "repository": {
27
- "url": "git+https://github.com/travetto/travetto.git",
28
- "directory": "module/model-sql"
30
+ "publishConfig": {
31
+ "access": "public"
29
32
  },
30
33
  "dependencies": {
31
- "@travetto/config": "^8.0.0-alpha.9",
32
- "@travetto/context": "^8.0.0-alpha.9",
33
- "@travetto/model": "^8.0.0-alpha.9",
34
- "@travetto/model-query": "^8.0.0-alpha.9"
34
+ "@travetto/config": "^8.0.1",
35
+ "@travetto/context": "^8.0.1",
36
+ "@travetto/model": "^8.0.1",
37
+ "@travetto/model-indexed": "^8.0.1",
38
+ "@travetto/model-query": "^8.0.1"
35
39
  },
36
40
  "peerDependencies": {
37
- "@travetto/cli": "^8.0.0-alpha.14",
38
- "@travetto/test": "^8.0.0-alpha.9"
41
+ "@travetto/cli": "^8.0.1",
42
+ "@travetto/test": "^8.0.1"
39
43
  },
40
44
  "peerDependenciesMeta": {
41
45
  "@travetto/cli": {
@@ -47,8 +51,5 @@
47
51
  },
48
52
  "travetto": {
49
53
  "displayName": "SQL Model Service"
50
- },
51
- "publishConfig": {
52
- "access": "public"
53
54
  }
54
55
  }
@@ -0,0 +1,218 @@
1
+ import { type AsyncContext, AsyncContextValue } from '@travetto/context';
2
+ import { ModelRegistryIndex, type ModelType } from '@travetto/model';
3
+ import { type AsyncMethodDescriptor, type Class, castTo, Util } from '@travetto/runtime';
4
+
5
+ import type { AbstractANSI99Dialect } from './dialect.ts';
6
+ import { SQLModelSchemaUtil } from './schema.ts';
7
+ import type { TableContext } from './types.ts';
8
+
9
+ export type TransactionType = 'required' | 'isolated' | 'force';
10
+
11
+ export interface ConnectionAware {
12
+ connection: SQLConnection;
13
+ }
14
+
15
+ /**
16
+ * Decorator to ensure a method runs inside a database transaction
17
+ */
18
+ export function Transactional(mode: TransactionType = 'required') {
19
+ return function <Target extends ConnectionAware>(target: Target, property: string, descriptor: AsyncMethodDescriptor<Target>): void {
20
+ const originalMethod = descriptor.value!;
21
+ descriptor.value = function (...args: unknown[]): ReturnType<typeof originalMethod> {
22
+ return this.connection.runWithTransaction(mode, () => originalMethod.call(this, ...args));
23
+ };
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Base abstract connection manager for SQL Model services.
29
+ * Uses @travetto/context to track active client connections and transaction state.
30
+ */
31
+ export abstract class SQLConnection<
32
+ C = unknown,
33
+ F extends { namespace?: string; database?: string } = { namespace?: string; database?: string }
34
+ > {
35
+ isolatedTransactions = true;
36
+ nestedTransactions = true;
37
+
38
+ abstract readonly dialect: AbstractANSI99Dialect;
39
+
40
+ readonly context: AsyncContext;
41
+
42
+ #activeConnection = new AsyncContextValue<C>(this, { failIfUnbound: { read: false } });
43
+ #activeTransaction = new AsyncContextValue<boolean>(this, { failIfUnbound: { read: false } });
44
+
45
+ constructor(context: AsyncContext) {
46
+ this.context = context;
47
+ }
48
+
49
+ abstract config: F;
50
+
51
+ getContext<T extends ModelType>(modelClass: Class<T>): TableContext<T> {
52
+ let tableName = ModelRegistryIndex.getStoreName(modelClass);
53
+ if (this.config.namespace) {
54
+ tableName = `${this.config.namespace}_${tableName}`;
55
+ }
56
+ return { tableName, ...SQLModelSchemaUtil.getSchemaContext(modelClass) };
57
+ }
58
+
59
+ get active(): C | undefined {
60
+ return this.#activeConnection.get();
61
+ }
62
+
63
+ get activeTransaction(): boolean {
64
+ return !!this.#activeTransaction.get();
65
+ }
66
+
67
+ /**
68
+ * Initializes the connection
69
+ */
70
+ abstract init(): Promise<void> | void;
71
+
72
+ /**
73
+ * Acquires a client connection
74
+ */
75
+ abstract acquire(): Promise<C>;
76
+
77
+ /**
78
+ * Releases a client connection
79
+ */
80
+ abstract release(connection: C): void;
81
+
82
+ /**
83
+ * Executes a SQL query
84
+ */
85
+ abstract execute<Type = unknown>(query: string, values?: unknown[]): Promise<{ count: number; records: Type[] }>;
86
+
87
+ /**
88
+ * Runs an operation with an active connection (allocating one if not already active)
89
+ */
90
+ async runWithConnection<Result>(operation: () => Promise<Result>): Promise<Result> {
91
+ if (this.active) {
92
+ return await operation();
93
+ }
94
+
95
+ return this.context.run(async () => {
96
+ let connection: C | undefined;
97
+ try {
98
+ connection = await this.acquire();
99
+ this.#activeConnection.set(connection);
100
+ return await operation();
101
+ } finally {
102
+ if (connection) {
103
+ this.release(connection);
104
+ }
105
+ }
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Iterates with an active connection
111
+ */
112
+ async *iterateWithConnection<Result>(operation: () => AsyncIterable<Result>): AsyncIterable<Result> {
113
+ if (this.active) {
114
+ yield* operation();
115
+ return;
116
+ }
117
+
118
+ const self = castTo<SQLConnection<C>>(this);
119
+ yield* this.context.iterate(async function* () {
120
+ let connection: C | undefined;
121
+ try {
122
+ connection = await self.acquire();
123
+ self.#activeConnection.set(connection);
124
+ yield* operation();
125
+ } finally {
126
+ if (connection) {
127
+ self.release(connection);
128
+ }
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Runs an operation within a database transaction context
135
+ */
136
+ async runWithTransaction<Result>(mode: TransactionType, operation: () => Promise<Result>): Promise<Result> {
137
+ if (this.activeTransaction) {
138
+ if (mode === 'isolated' || mode === 'force') {
139
+ const transactionId = mode === 'isolated' ? `tx${Util.uuid()}` : undefined;
140
+ try {
141
+ await this.startTransaction(transactionId);
142
+ const result = await operation();
143
+ await this.commitTransaction(transactionId);
144
+ return result;
145
+ } catch (error) {
146
+ try {
147
+ await this.rollbackTransaction(transactionId);
148
+ } catch {}
149
+ throw error;
150
+ }
151
+ } else {
152
+ return await operation();
153
+ }
154
+ } else {
155
+ return this.runWithConnection(async () => {
156
+ this.#activeTransaction.set(true);
157
+ if (this.isolatedTransactions) {
158
+ await this.execute(this.dialect.transactionStatements.isolate);
159
+ }
160
+ await this.execute(this.dialect.transactionStatements.begin);
161
+ try {
162
+ const result = await operation();
163
+ await this.execute(this.dialect.transactionStatements.commit);
164
+ return result;
165
+ } catch (error) {
166
+ try {
167
+ await this.execute(this.dialect.transactionStatements.rollback);
168
+ } catch {}
169
+ throw error;
170
+ } finally {
171
+ this.#activeTransaction.set(false);
172
+ }
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Starts a transaction or SAVEPOINT
179
+ */
180
+ async startTransaction(transactionId?: string): Promise<void> {
181
+ if (transactionId) {
182
+ if (this.nestedTransactions) {
183
+ await this.execute(this.dialect.transactionStatements.beginNested, [transactionId]);
184
+ }
185
+ } else {
186
+ if (this.isolatedTransactions) {
187
+ await this.execute(this.dialect.transactionStatements.isolate);
188
+ }
189
+ await this.execute(this.dialect.transactionStatements.begin);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Commits the transaction or release SAVEPOINT
195
+ */
196
+ async commitTransaction(transactionId?: string): Promise<void> {
197
+ if (transactionId) {
198
+ if (this.nestedTransactions) {
199
+ await this.execute(this.dialect.transactionStatements.commitNested, [transactionId]);
200
+ }
201
+ } else {
202
+ await this.execute(this.dialect.transactionStatements.commit);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Rolls back the transaction or SAVEPOINT
208
+ */
209
+ async rollbackTransaction(transactionId?: string): Promise<void> {
210
+ if (transactionId) {
211
+ if (this.nestedTransactions) {
212
+ await this.execute(this.dialect.transactionStatements.rollbackNested, [transactionId]);
213
+ }
214
+ } else {
215
+ await this.execute(this.dialect.transactionStatements.rollback);
216
+ }
217
+ }
218
+ }