@deterministic-code/datasources 0.0.0-stage → 0.1.0

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.
Files changed (209) hide show
  1. package/package.json +16 -4
  2. package/src/ArrayReader.ts +32 -0
  3. package/src/BatchCommandIBulkWriteStrategy.ts +228 -0
  4. package/src/BuiltRepository.ts +17 -0
  5. package/src/BulkCRUDRepository.ts +84 -0
  6. package/src/BulkCommand.ts +6 -0
  7. package/src/BulkWriteEnrichment.ts +19 -0
  8. package/src/CRUDRepository.ts +264 -0
  9. package/src/CRUDRepositoryBuilder.ts +53 -0
  10. package/src/CRUDRepositoryOptions.ts +8 -0
  11. package/src/CRUDRepositoryStatements.ts +43 -0
  12. package/src/CascadeDeleteEnrichmentStrategy.ts +51 -0
  13. package/src/CascadeDeleteUpdateEnrichmentStrategy.ts +28 -0
  14. package/src/CommandMiddlewareHost.ts +113 -0
  15. package/src/ConsoleCommandMiddleware.ts +64 -0
  16. package/src/CreateEnrichmentStrategy.ts +82 -0
  17. package/src/Datasource.ts +152 -0
  18. package/src/DatasourceError.ts +6 -0
  19. package/src/DatasourceFactory.ts +86 -0
  20. package/src/DatasourceOptions.ts +1 -0
  21. package/src/DatasourceUrl.ts +37 -0
  22. package/src/DatasourceUrlParser.ts +188 -0
  23. package/src/DefaultCreateEnrichmentStrategy.ts +21 -0
  24. package/src/DefaultReadonlyEnrichmentStrategy.ts +164 -0
  25. package/src/DefaultUpdateEnrichmentStrategy.ts +40 -0
  26. package/src/DefaultWriteEnrichmentStrategy.ts +151 -0
  27. package/src/DeleteEnrichmentStrategy.ts +56 -0
  28. package/src/DriverLoader.ts +97 -0
  29. package/src/Enrichment.ts +15 -0
  30. package/src/EnrichmentPropertyType.ts +9 -0
  31. package/src/ExpectConsoleMiddleware.ts +47 -0
  32. package/src/FirstResultReader.ts +49 -0
  33. package/src/HashsetOptimizedReadonlyEnrichmentStrategy.ts +80 -0
  34. package/src/IBulkWriteStrategy.ts +13 -0
  35. package/src/ICRUDRepository.ts +13 -0
  36. package/src/ICRUDRepositoryFactory.ts +8 -0
  37. package/src/ICommand.ts +45 -0
  38. package/src/ICommandMiddleware.ts +20 -0
  39. package/src/IConnection.ts +13 -0
  40. package/src/IDatasource.ts +27 -0
  41. package/src/IDatasourceFactory.ts +6 -0
  42. package/src/IDriver.ts +24 -0
  43. package/src/IEnriched.ts +11 -0
  44. package/src/IReader.ts +8 -0
  45. package/src/IReadonlyEnrichmentStrategy.ts +19 -0
  46. package/src/IReadonlyRepository.ts +21 -0
  47. package/src/IRepository.ts +5 -0
  48. package/src/IRepositoryFactory.ts +8 -0
  49. package/src/IRepositoryTree.ts +5 -0
  50. package/src/IRepositoryTreeNode.ts +8 -0
  51. package/src/ITransaction.ts +9 -0
  52. package/src/IUnitOfWork.ts +5 -0
  53. package/src/IWriteEnrichmentStrategy.ts +8 -0
  54. package/src/Information.ts +5 -0
  55. package/src/IsolationLevel.ts +8 -0
  56. package/src/LazyWriteEnrichmentStrategy.ts +5 -0
  57. package/src/NestedToObjectEnrichmentStrategy.ts +33 -0
  58. package/src/Parameter.ts +8 -0
  59. package/src/ParameterDirection.ts +9 -0
  60. package/src/ProviderCatalog.ts +21 -0
  61. package/src/ProviderNotImplementedError.ts +9 -0
  62. package/src/ProviderNotInstalledError.ts +9 -0
  63. package/src/ReadonlyRepository.ts +276 -0
  64. package/src/ReadonlyRepositoryBuilder.ts +53 -0
  65. package/src/ReadonlyRepositoryMappingOptions.ts +15 -0
  66. package/src/ReadonlyRepositoryStatements.ts +13 -0
  67. package/src/RepositoryAccess.ts +14 -0
  68. package/src/RepositoryFactory.ts +115 -0
  69. package/src/RepositoryNotRegisteredError.ts +9 -0
  70. package/src/RepositoryTree.ts +6 -0
  71. package/src/RepositoryTreeNode.ts +11 -0
  72. package/src/Response.ts +15 -0
  73. package/src/Row.ts +3 -0
  74. package/src/SingleCommandIBulkWriteStrategy.ts +35 -0
  75. package/src/StatementMappers.ts +110 -0
  76. package/src/StoredEnrichment.ts +25 -0
  77. package/src/StoredProcedureBulkWriteStrategy.ts +164 -0
  78. package/src/StreamingReader.ts +64 -0
  79. package/src/UnitOfWorkTransaction.ts +50 -0
  80. package/src/UpdateEnrichmentStrategy.ts +97 -0
  81. package/src/WriteMode.ts +1 -0
  82. package/src/builders/Field.ts +6 -0
  83. package/src/builders/Scalar.ts +1 -0
  84. package/src/builders/index.ts +2 -0
  85. package/src/collectAsync.ts +7 -0
  86. package/src/converters/Cell.ts +10 -0
  87. package/src/converters/ChangeCasingConverter.ts +59 -0
  88. package/src/converters/Constructable.ts +1 -0
  89. package/src/converters/MetaDataRowTypeConverter.ts +80 -0
  90. package/src/converters/PluralizerConverter.ts +67 -0
  91. package/src/converters/RegexConverter.ts +14 -0
  92. package/src/converters/ReplaceConverter.ts +6 -0
  93. package/src/converters/RowTypeConverter.ts +33 -0
  94. package/src/converters/ToLowerCaseConverter.ts +4 -0
  95. package/src/converters/ToUpperCaseConverter.ts +4 -0
  96. package/src/converters/TypeConverter.ts +6 -0
  97. package/src/createCRUDRepositoryOptions.ts +21 -0
  98. package/src/createReadonlyRepositoryMappingOptions.ts +26 -0
  99. package/src/dotEnv.ts +16 -0
  100. package/src/index.ts +222 -0
  101. package/src/mappers/ChangeCasingMapper.ts +15 -0
  102. package/src/mappers/DictionaryMapper.ts +16 -0
  103. package/src/mappers/FlatToNestedMapper.ts +127 -0
  104. package/src/mappers/IFlatToNestedMapper.ts +7 -0
  105. package/src/mappers/IMapper.ts +4 -0
  106. package/src/mappers/IdentityMapper.ts +6 -0
  107. package/src/mappers/PluralizerMapper.ts +9 -0
  108. package/src/mappers/RowTypeMapper.ts +13 -0
  109. package/src/mappers/TransformConverterMapper.ts +11 -0
  110. package/src/mappers/TransformMapper.ts +8 -0
  111. package/src/mappers/TypeMap.ts +10 -0
  112. package/src/mappers/TypeMapGroup.ts +6 -0
  113. package/src/mocks/index.ts +9 -0
  114. package/src/mocks/mock-command.ts +66 -0
  115. package/src/mocks/mock-connection.ts +57 -0
  116. package/src/mocks/mock-datasource.ts +65 -0
  117. package/src/mocks/mock-driver.ts +27 -0
  118. package/src/mocks/mock-reader.ts +38 -0
  119. package/src/mocks/mock-repository.ts +91 -0
  120. package/src/mocks/mock-select-statement.ts +17 -0
  121. package/src/mocks/mock-transaction.ts +31 -0
  122. package/src/mocks/mock-where.ts +13 -0
  123. package/src/parametersToRecord.ts +11 -0
  124. package/src/readerToAsyncIterable.ts +11 -0
  125. package/src/splitStatements.ts +99 -0
  126. package/src/statementParameters.ts +22 -0
  127. package/src/statementUsesParameter.ts +4 -0
  128. package/src/statements/Aggregate.ts +9 -0
  129. package/src/statements/ArrayExpression.ts +5 -0
  130. package/src/statements/BulkDeleteStatement.ts +18 -0
  131. package/src/statements/BulkDeleteStatementTextRenderer.ts +16 -0
  132. package/src/statements/BulkInsertStatement.ts +37 -0
  133. package/src/statements/BulkInsertStatementTextRenderer.ts +10 -0
  134. package/src/statements/BulkUpdateStatement.ts +24 -0
  135. package/src/statements/BulkUpdateStatementTextRenderer.ts +29 -0
  136. package/src/statements/CheckConstraint.ts +9 -0
  137. package/src/statements/Cmp.ts +12 -0
  138. package/src/statements/Column.ts +10 -0
  139. package/src/statements/ColumnDefault.ts +6 -0
  140. package/src/statements/Condition.ts +3 -0
  141. package/src/statements/ConditionRenderer.ts +17 -0
  142. package/src/statements/CreateIndexStatement.ts +18 -0
  143. package/src/statements/CreateIndexStatementTextRenderer.ts +11 -0
  144. package/src/statements/CreateTableStatement.ts +18 -0
  145. package/src/statements/CreateTableStatementTextRenderer.ts +92 -0
  146. package/src/statements/DefaultStatementTextRenderer.ts +9 -0
  147. package/src/statements/DeleteStatement.ts +15 -0
  148. package/src/statements/DeleteStatementTextRenderer.ts +12 -0
  149. package/src/statements/ExecuteStoredProcedureStatement.ts +15 -0
  150. package/src/statements/ExecuteStoredProcedureStatementTextRenderer.ts +15 -0
  151. package/src/statements/ExpressionEvaluator.ts +166 -0
  152. package/src/statements/ExpressionRenderer.ts +161 -0
  153. package/src/statements/ForeignKey.ts +23 -0
  154. package/src/statements/IBulkDeleteStatement.ts +3 -0
  155. package/src/statements/IBulkInsertStatement.ts +3 -0
  156. package/src/statements/IBulkUpdateStatement.ts +3 -0
  157. package/src/statements/ICondition.ts +5 -0
  158. package/src/statements/IConditionVisitor.ts +7 -0
  159. package/src/statements/ICreateIndexStatement.ts +3 -0
  160. package/src/statements/ICreateTableStatement.ts +3 -0
  161. package/src/statements/IDeleteStatement.ts +6 -0
  162. package/src/statements/IExecuteStoredProcedureStatement.ts +3 -0
  163. package/src/statements/IExpression.ts +1 -0
  164. package/src/statements/IInsertStatement.ts +3 -0
  165. package/src/statements/IReturnStatement.ts +3 -0
  166. package/src/statements/ISelectStatement.ts +6 -0
  167. package/src/statements/IStatement.ts +5 -0
  168. package/src/statements/IStatementVisitor.ts +27 -0
  169. package/src/statements/ITableConstraint.ts +6 -0
  170. package/src/statements/IUpdateStatement.ts +6 -0
  171. package/src/statements/IWhere.ts +7 -0
  172. package/src/statements/Identifier.ts +5 -0
  173. package/src/statements/InsertStatement.ts +15 -0
  174. package/src/statements/InsertStatementTextRenderer.ts +7 -0
  175. package/src/statements/Join.ts +26 -0
  176. package/src/statements/Literal.ts +6 -0
  177. package/src/statements/Native.ts +5 -0
  178. package/src/statements/NotNull.ts +3 -0
  179. package/src/statements/Operator.ts +234 -0
  180. package/src/statements/OrderBy.ts +19 -0
  181. package/src/statements/PrimaryKey.ts +9 -0
  182. package/src/statements/ReturnStatement.ts +15 -0
  183. package/src/statements/ReturnStatementTextRenderer.ts +15 -0
  184. package/src/statements/SelectInsertedIdStatement.ts +11 -0
  185. package/src/statements/SelectInsertedIdStatementTextRenderer.ts +7 -0
  186. package/src/statements/SelectStatement.ts +34 -0
  187. package/src/statements/SelectStatementOptions.ts +16 -0
  188. package/src/statements/SelectStatementTextRenderer.ts +71 -0
  189. package/src/statements/StatementEvaluator.ts +5 -0
  190. package/src/statements/StatementParser.ts +461 -0
  191. package/src/statements/StatementRenderer.ts +120 -0
  192. package/src/statements/StatementTextRenderer.ts +60 -0
  193. package/src/statements/Subquery.ts +6 -0
  194. package/src/statements/UniqueConstraint.ts +9 -0
  195. package/src/statements/UpdateStatement.ts +34 -0
  196. package/src/statements/UpdateStatementTextRenderer.ts +63 -0
  197. package/src/statements/Where.ts +250 -0
  198. package/src/statements/WhereText.ts +10 -0
  199. package/src/statements/index.ts +75 -0
  200. package/src/stubs/StubCommand.ts +30 -0
  201. package/src/stubs/StubConnection.ts +21 -0
  202. package/src/stubs/StubDatasource.ts +60 -0
  203. package/src/stubs/StubReader.ts +25 -0
  204. package/src/stubs/StubRepository.ts +42 -0
  205. package/src/stubs/StubTransaction.ts +17 -0
  206. package/src/stubs/createStubDatasource.ts +4 -0
  207. package/src/stubs/index.ts +7 -0
  208. package/src/stubs/notImplemented.ts +5 -0
  209. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "@deterministic-code/datasources",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./mocks": "./src/mocks/index.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "license": "MIT"
18
+ }
@@ -0,0 +1,32 @@
1
+ import type { IReader } from "./IReader.ts";
2
+ import { readerToAsyncIterable } from "./readerToAsyncIterable.ts";
3
+
4
+ export class ArrayReader<T = unknown> implements IReader<T> {
5
+ private index = -1;
6
+ current: T | undefined = undefined;
7
+
8
+ constructor(private readonly rows: readonly T[]) {}
9
+
10
+ async next(): Promise<boolean> {
11
+ this.index += 1;
12
+ if (this.index < this.rows.length) {
13
+ this.current = this.rows[this.index];
14
+ return true;
15
+ }
16
+ this.current = undefined;
17
+ return false;
18
+ }
19
+
20
+ read(): AsyncIterable<T> {
21
+ return readerToAsyncIterable(this);
22
+ }
23
+
24
+ async nextResult(): Promise<boolean> {
25
+ this.current = undefined;
26
+ return false;
27
+ }
28
+
29
+ async dispose(): Promise<void> {}
30
+
31
+ [Symbol.asyncDispose] = (): Promise<void> => this.dispose();
32
+ }
@@ -0,0 +1,228 @@
1
+ import type { CRUDRepository } from "./CRUDRepository.ts";
2
+ import type { CRUDRepositoryOptions } from "./CRUDRepositoryOptions.ts";
3
+ import type { Response } from "./Response.ts";
4
+ import type { IWhere } from "./statements/IWhere.ts";
5
+ import type { BulkCommand } from "./BulkCommand.ts";
6
+ import { Native } from "./statements/Native.ts";
7
+ import type { IBulkWriteStrategy } from "./IBulkWriteStrategy.ts";
8
+ import { execute, type ICommand } from "./ICommand.ts";
9
+ import type { Cell, Cells } from "./converters/Cell.ts";
10
+ import type { Field } from "./builders/Field.ts";
11
+ import type { Scalar } from "./builders/Scalar.ts";
12
+ import type { IExpression } from "./statements/IExpression.ts";
13
+ import {
14
+ BulkDeleteStatement,
15
+ BulkInsertStatement,
16
+ BulkUpdateStatement,
17
+ DefaultStatementTextRenderer,
18
+ Literal,
19
+ Operator,
20
+ Where,
21
+ } from "./statements/index.ts";
22
+ import { DatasourceError } from "./DatasourceError.ts";
23
+ import { repositoryParts } from "./CRUDRepositoryBuilder.ts";
24
+ import { MetaDataRowTypeConverter } from "./converters/MetaDataRowTypeConverter.ts";
25
+ import type { Parameter } from "./Parameter.ts";
26
+ import { afterBulkWrite } from "./BulkWriteEnrichment.ts";
27
+
28
+ const empty = (): Response => ({ result: null, rowsAffected: 0 });
29
+ const budget = 8000;
30
+ const maxRows = 250;
31
+
32
+ const assigned = (value: Cell): boolean =>
33
+ value !== null && value !== "" && value !== 0 && value !== 0n;
34
+
35
+ const columnsOf = (rows: readonly { name: string }[][]): string[] => {
36
+ const seen = new Set<string>();
37
+ const columns: string[] = [];
38
+ for (const row of rows)
39
+ for (const field of row)
40
+ if (!seen.has(field.name)) {
41
+ seen.add(field.name);
42
+ columns.push(field.name);
43
+ }
44
+ return columns;
45
+ };
46
+
47
+ type Inserted = { table: string; fields: Parameter[] };
48
+ type Updated = { table: string; fields: Parameter[]; ids: Parameter[] };
49
+ type Deleted = { table: string; ids: Parameter[] };
50
+
51
+ export class BatchCommandIBulkWriteStrategy<T extends object> implements IBulkWriteStrategy<T> {
52
+ repo!: CRUDRepository<T>;
53
+ command!: ICommand;
54
+ options!: CRUDRepositoryOptions<T>;
55
+ private cached?: ReturnType<typeof repositoryParts>["statements"];
56
+
57
+ attach = (command: ICommand, options: CRUDRepositoryOptions<T>): void => {
58
+ this.command = command;
59
+ this.options = options;
60
+ };
61
+
62
+ create = (items: T[]): Promise<Response> => this.then(this.writeCreates(items), items, "create");
63
+
64
+ update = (items: T[], byWhere: IWhere | undefined): Promise<Response> =>
65
+ this.then(
66
+ byWhere ? this.sendEach(items.map((item) => this.updateCommand(item, byWhere))) : this.writeUpdates(items),
67
+ items,
68
+ "update",
69
+ );
70
+
71
+ delete = (items: T[], byWhere: IWhere | undefined): Promise<Response> =>
72
+ this.then(
73
+ byWhere ? this.sendEach(items.map((item) => this.deleteCommand(item, byWhere))) : this.writeDeletes(items),
74
+ items,
75
+ "delete",
76
+ );
77
+
78
+ private then = async (write: Promise<Response>, items: T[], mode: "create" | "update" | "delete"): Promise<Response> => {
79
+ const response = await write;
80
+ await afterBulkWrite(this.repo, this.options, items, mode);
81
+ return response;
82
+ };
83
+
84
+ private writeCreates = async (items: T[]): Promise<Response> => {
85
+ if (items.length === 0) return empty();
86
+ const rows = items.map(this.mappedInsert);
87
+ const columns = columnsOf(rows.map((row) => row.fields));
88
+ return this.chunked(rows, columns.length, (chunk) => {
89
+ const statement = new BulkInsertStatement(
90
+ rows[0]!.table,
91
+ columns,
92
+ chunk.map((row) => row.fields),
93
+ );
94
+ return execute(this.command, statement, statement.parameters);
95
+ });
96
+ };
97
+
98
+ private writeUpdates = async (items: T[]): Promise<Response> => {
99
+ if (items.length === 0) return empty();
100
+ const rows = items.map(this.mappedUpdate);
101
+ const set = columnsOf(rows.map((row) => row.fields));
102
+ const ids = columnsOf(rows.map((row) => row.ids));
103
+ return this.chunked(rows, set.length + ids.length, (chunk) => {
104
+ const statement = new BulkUpdateStatement(rows[0]!.table, set, ids, chunk);
105
+ return execute(this.command, statement, statement.parameters);
106
+ });
107
+ };
108
+
109
+ private writeDeletes = async (items: T[]): Promise<Response> => {
110
+ if (items.length === 0) return empty();
111
+ const rows = items.map(this.mappedDelete);
112
+ const ids = columnsOf(rows.map((row) => row.ids));
113
+ return this.chunked(rows, ids.length, (chunk) => {
114
+ const statement = new BulkDeleteStatement(
115
+ rows[0]!.table,
116
+ ids,
117
+ chunk.map((row) => row.ids),
118
+ );
119
+ return execute(this.command, statement, statement.parameters);
120
+ });
121
+ };
122
+
123
+ private chunked = async <TRow>(
124
+ rows: TRow[],
125
+ width: number,
126
+ write: (chunk: TRow[]) => Promise<Response>,
127
+ ): Promise<Response> => {
128
+ const size = Math.min(maxRows, Math.max(1, Math.floor(budget / Math.max(1, width))));
129
+ let rowsAffected = 0;
130
+ for (let at = 0; at < rows.length; at += size) rowsAffected += (await write(rows.slice(at, at + size))).rowsAffected;
131
+ return { result: null, rowsAffected };
132
+ };
133
+
134
+ private sendEach = async (commands: BulkCommand[]): Promise<Response> => {
135
+ let rowsAffected = 0;
136
+ for (const command of commands)
137
+ rowsAffected += (await this.command.executeNative(new Native(command.text), command.parameters)).rowsAffected;
138
+ return { result: null, rowsAffected };
139
+ };
140
+
141
+ private mappedInsert = (item: T): Inserted => {
142
+ const record = this.reverse(item);
143
+ const statement = this.statements().insert(
144
+ this.fields(record, (col) => !this.ids.includes(col) || assigned(record[col] ?? null)),
145
+ );
146
+ return { table: statement.table, fields: statement.fields };
147
+ };
148
+
149
+ private mappedUpdate = (item: T): Updated => {
150
+ const record = this.reverse(item);
151
+ const fields = this.fields(record, (col) => !this.ids.includes(col));
152
+ if (fields.length === 0) throw new DatasourceError("Update requires at least one non-Id field");
153
+ const statement = this.statements().update(fields, this.idsWhere(record), (name, value) =>
154
+ this.cell(name, value, true),
155
+ );
156
+ return { table: statement.table, fields: statement.fields, ids: statement.where?.parameters ?? [] };
157
+ };
158
+
159
+ private mappedDelete = (item: T): Deleted => {
160
+ const record = this.reverse(item);
161
+ const statement = this.statements().delete(this.idsWhere(record), (name, value) => this.cell(name, value, true));
162
+ return { table: statement.table, ids: statement.where?.parameters ?? [] };
163
+ };
164
+
165
+ private updateCommand = (item: T, byWhere?: IWhere): BulkCommand => {
166
+ const record = this.reverse(item);
167
+ const fields = this.fields(record, (col) => !this.ids.includes(col));
168
+ if (fields.length === 0) throw new DatasourceError("Update requires at least one non-Id field");
169
+ const statement = this.statements().update(fields, byWhere ?? this.idsWhere(record), (name, value) =>
170
+ this.cell(name, value, byWhere === undefined),
171
+ );
172
+ return { text: DefaultStatementTextRenderer.instance.render(statement), parameters: statement.parameters };
173
+ };
174
+
175
+ private deleteCommand = (item: T, byWhere?: IWhere): BulkCommand => {
176
+ const record = this.reverse(item);
177
+ const statement = this.statements().delete(byWhere ?? this.idsWhere(record), (name, value) =>
178
+ this.cell(name, value, byWhere === undefined),
179
+ );
180
+ const listed = statement.where?.parameters ?? [];
181
+ const parameters: Parameter[] = listed.length
182
+ ? listed
183
+ : DefaultStatementTextRenderer.instance.render(statement).includes("$")
184
+ ? this.fields(record, () => true)
185
+ : [];
186
+ return { text: DefaultStatementTextRenderer.instance.render(statement), parameters };
187
+ };
188
+
189
+ private statements = () =>
190
+ (this.cached ??= repositoryParts(this.command.connection, this.options).statements);
191
+
192
+ private get ids(): readonly string[] {
193
+ return this.options.ids ?? this.repo.ids;
194
+ }
195
+
196
+ private reverse = (item: T): Cells => {
197
+ const objects = this.options.objectConverter ?? new MetaDataRowTypeConverter<T>();
198
+ const row = objects.reverseConvert(item);
199
+ return this.options.rowConverter ? this.options.rowConverter.reverseConvert(row) : row;
200
+ };
201
+
202
+ private fields = (record: Cells, include: (col: string) => boolean): Field[] =>
203
+ Object.entries(record)
204
+ .filter(([key, value]) => include(key) && value !== null && value !== undefined)
205
+ .sort(([left], [right]) => left.localeCompare(right))
206
+ .map(([name, value]) => ({ name, value: this.cell(name, value, true) as Scalar }));
207
+
208
+ private cell = (property: string, value: Cell, fieldsApplied: boolean): Cell =>
209
+ fieldsApplied
210
+ ? value
211
+ : this.options.objectConverter instanceof MetaDataRowTypeConverter &&
212
+ this.options.objectConverter.fields[property]
213
+ ? this.options.objectConverter.fields[property]!.reverseConvert(value)
214
+ : value;
215
+
216
+ private idsWhere = (record: Cells): IWhere => {
217
+ const parts: IExpression[] = this.ids.map((col) => {
218
+ if (!Object.prototype.hasOwnProperty.call(record, col))
219
+ throw new DatasourceError(`Missing Id value "${col}"`);
220
+ return new Operator.Equals(col, record[col] as Scalar);
221
+ });
222
+ return new Where(
223
+ parts.length === 0
224
+ ? new Operator.Equals(new Literal(1), new Literal(1))
225
+ : parts.reduce((left, right) => new Operator.And(left, right)),
226
+ );
227
+ };
228
+ }
@@ -0,0 +1,17 @@
1
+ import type { IConnection } from "./IConnection.ts";
2
+ import type { Cells } from "./converters/Cell.ts";
3
+ import type { Constructable } from "./converters/Constructable.ts";
4
+ import type { TypeConverter } from "./converters/TypeConverter.ts";
5
+ import type { IMapper } from "./mappers/IMapper.ts";
6
+ import type { ReadonlyRepositoryStatements } from "./ReadonlyRepositoryStatements.ts";
7
+
8
+ export type BuiltRepository<T = Cells> = {
9
+ connection: IConnection;
10
+ rowConverter?: TypeConverter<Cells, Cells>;
11
+ objectConverter: TypeConverter<T, Cells>;
12
+ propertyMappings: IMapper<string, string>;
13
+ entityMapper: IMapper<string, string>;
14
+ statements: ReadonlyRepositoryStatements;
15
+ ids: string[];
16
+ type: Constructable<T>;
17
+ };
@@ -0,0 +1,84 @@
1
+ import type { IConnection } from "./IConnection.ts";
2
+ import type { Constructable } from "./converters/Constructable.ts";
3
+ import type { Cells } from "./converters/Cell.ts";
4
+ import { CRUDRepository } from "./CRUDRepository.ts";
5
+ import type { CRUDRepositoryOptions } from "./CRUDRepositoryOptions.ts";
6
+ import { createCRUDRepositoryOptions } from "./createCRUDRepositoryOptions.ts";
7
+ import type { Response } from "./Response.ts";
8
+ import type { IWhere } from "./statements/IWhere.ts";
9
+ import type { IBulkWriteStrategy } from "./IBulkWriteStrategy.ts";
10
+ import { SingleCommandIBulkWriteStrategy } from "./SingleCommandIBulkWriteStrategy.ts";
11
+ import { UnitOfWorkTransaction } from "./UnitOfWorkTransaction.ts";
12
+
13
+ const isBulkStrategy = <T extends object>(value: object): value is IBulkWriteStrategy<T> =>
14
+ "create" in value &&
15
+ "update" in value &&
16
+ "delete" in value &&
17
+ !("persist" in value) &&
18
+ !("createCommand" in value) &&
19
+ !("statements" in value);
20
+
21
+ const transactionOf = <T extends object>(
22
+ options: IConnection | CRUDRepositoryOptions<T> | IBulkWriteStrategy<T> | undefined,
23
+ ): boolean =>
24
+ options !== undefined && "write" in options ? options.write?.useTransaction ?? false : false;
25
+
26
+ const inTransaction = async <TResult>(
27
+ transactional: boolean,
28
+ connection: IConnection,
29
+ write: () => Promise<TResult>,
30
+ ): Promise<TResult> => {
31
+ if (!transactional) return write();
32
+ const work = new UnitOfWorkTransaction(connection);
33
+ await work.begin();
34
+ try {
35
+ const result = await write();
36
+ await work.commit();
37
+ return result;
38
+ } catch (error) {
39
+ await work.abort();
40
+ throw error;
41
+ }
42
+ };
43
+
44
+ export class BulkCRUDRepository<T extends object = Cells> extends CRUDRepository<T> {
45
+ private readonly strategy: IBulkWriteStrategy<T>;
46
+ private readonly transactional: boolean;
47
+
48
+ constructor(type: Constructable<T>, connection: IConnection, bulk?: IBulkWriteStrategy<T>);
49
+ constructor(connection: IConnection, options: CRUDRepositoryOptions<T>, bulk?: IBulkWriteStrategy<T>);
50
+ constructor(
51
+ typeOrConnection: Constructable<T> | IConnection,
52
+ connectionOrOptions?: IConnection | CRUDRepositoryOptions<T> | IBulkWriteStrategy<T>,
53
+ bulk?: IBulkWriteStrategy<T>,
54
+ ) {
55
+ const strategy = bulk ?? (isBulkStrategy<T>(connectionOrOptions ?? {}) ? connectionOrOptions : undefined);
56
+ const transactional = transactionOf(connectionOrOptions);
57
+ const parent = (
58
+ isBulkStrategy(connectionOrOptions ?? {}) ? undefined : connectionOrOptions
59
+ ) as IConnection & CRUDRepositoryOptions<T>;
60
+ super(typeOrConnection as Constructable<T> & IConnection, parent);
61
+ this.strategy = strategy ?? new SingleCommandIBulkWriteStrategy<T>();
62
+ this.transactional = transactional;
63
+ this.strategy.repo = this;
64
+ this.strategy.attach(
65
+ this.command(),
66
+ typeof typeOrConnection === "function"
67
+ ? createCRUDRepositoryOptions(typeOrConnection)
68
+ : parent,
69
+ );
70
+ }
71
+
72
+ createBulk = async (items: T[]): Promise<T[]> => {
73
+ await inTransaction(this.transactional || items.length > 0, this.command().connection, () =>
74
+ this.strategy.create(items),
75
+ );
76
+ return items;
77
+ };
78
+
79
+ updateBulk = (items: T[], byWhere?: IWhere): Promise<Response> =>
80
+ inTransaction(this.transactional, this.command().connection, () => this.strategy.update(items, byWhere));
81
+
82
+ deleteBulk = (items: T[], byWhere?: IWhere): Promise<Response> =>
83
+ inTransaction(this.transactional, this.command().connection, () => this.strategy.delete(items, byWhere));
84
+ }
@@ -0,0 +1,6 @@
1
+ import type { Parameter } from "./Parameter.ts";
2
+
3
+ export interface BulkCommand {
4
+ text: string;
5
+ parameters: Parameter[];
6
+ }
@@ -0,0 +1,19 @@
1
+ import type { CRUDRepository } from "./CRUDRepository.ts";
2
+ import type { CRUDRepositoryOptions } from "./CRUDRepositoryOptions.ts";
3
+ import { DefaultWriteEnrichmentStrategy } from "./DefaultWriteEnrichmentStrategy.ts";
4
+ import { enrichmentsOf } from "./ReadonlyRepository.ts";
5
+ import { RepositoryTree } from "./RepositoryTree.ts";
6
+ import { RepositoryTreeNode } from "./RepositoryTreeNode.ts";
7
+ import type { WriteMode } from "./WriteMode.ts";
8
+
9
+ export const afterBulkWrite = async <T extends object>(
10
+ repo: CRUDRepository<T>,
11
+ options: CRUDRepositoryOptions<T>,
12
+ items: T[],
13
+ mode: WriteMode,
14
+ ): Promise<void> => {
15
+ if (items.length === 0 || !enrichmentsOf(repo)?.length) return;
16
+ const write = options.write ?? new DefaultWriteEnrichmentStrategy<T>();
17
+ const tree = new RepositoryTree(new RepositoryTreeNode(repo, ""));
18
+ await (write.persistBulk ?? write.persist)(tree, items, mode);
19
+ };
@@ -0,0 +1,264 @@
1
+ import type { IConnection } from "./IConnection.ts";
2
+ import type { ICRUDRepository } from "./ICRUDRepository.ts";
3
+ import type { Response } from "./Response.ts";
4
+ import type { IWhere } from "./IWhere.ts";
5
+ import type { Field } from "./builders/Field.ts";
6
+ import type { Scalar } from "./builders/Scalar.ts";
7
+ import type { IExpression } from "./statements/IExpression.ts";
8
+ import { Literal } from "./statements/Literal.ts";
9
+ import { DefaultStatementTextRenderer, Operator, Where } from "./statements/index.ts";
10
+ import type { Cell, Cells } from "./converters/Cell.ts";
11
+ import type { Constructable } from "./converters/Constructable.ts";
12
+ import type { CRUDRepositoryOptions } from "./CRUDRepositoryOptions.ts";
13
+ import { repositoryParts } from "./CRUDRepositoryBuilder.ts";
14
+ import type { CRUDRepositoryStatements } from "./CRUDRepositoryStatements.ts";
15
+ import { MetaDataRowTypeConverter } from "./converters/MetaDataRowTypeConverter.ts";
16
+ import { enrichmentsOf, ReadonlyRepository } from "./ReadonlyRepository.ts";
17
+ import type { Enrichment } from "./Enrichment.ts";
18
+ import type { IReadonlyEnrichmentStrategy } from "./IReadonlyEnrichmentStrategy.ts";
19
+ import type { IReadonlyRepository } from "./IReadonlyRepository.ts";
20
+ import { DefaultReadonlyEnrichmentStrategy } from "./DefaultReadonlyEnrichmentStrategy.ts";
21
+ import { StoredEnrichment } from "./StoredEnrichment.ts";
22
+ import { execute, executeReader } from "./ICommand.ts";
23
+ import { DatasourceError } from "./DatasourceError.ts";
24
+ import { DefaultWriteEnrichmentStrategy } from "./DefaultWriteEnrichmentStrategy.ts";
25
+ import type { IWriteEnrichmentStrategy } from "./IWriteEnrichmentStrategy.ts";
26
+ import { UnitOfWorkTransaction } from "./UnitOfWorkTransaction.ts";
27
+ import type { IRepositoryTree } from "./IRepositoryTree.ts";
28
+ import { RepositoryTree } from "./RepositoryTree.ts";
29
+ import { RepositoryTreeNode } from "./RepositoryTreeNode.ts";
30
+
31
+ type Props = Record<string, Scalar | object>;
32
+
33
+ const assigned = (value: Cell): boolean =>
34
+ value !== null && value !== "" && value !== 0 && value !== 0n;
35
+
36
+ const fieldsOf = (
37
+ record: Cells,
38
+ include: (col: string) => boolean,
39
+ cell: (property: string, value: Cell, fieldsApplied: boolean) => Cell,
40
+ enrichmentInto: (name: string) => boolean,
41
+ ): Field[] =>
42
+ Object.entries(record)
43
+ .filter(([key, value]) => include(key) && !enrichmentInto(key) && value !== null && value !== undefined)
44
+ .sort(([left], [right]) => left.localeCompare(right))
45
+ .map(([name, value]) => ({ name, value: cell(name, value, true) as Scalar }));
46
+
47
+ const isCells = (id: Scalar | Cells): id is Cells => typeof id === "object" && id !== null;
48
+
49
+ const named = (row: Cells, property: string, sqlColumn: string): Cells => {
50
+ if (property === sqlColumn || !(sqlColumn in row)) return row;
51
+ const { [sqlColumn]: cell, ...rest } = row;
52
+ return { ...rest, [property]: cell };
53
+ };
54
+
55
+ const identified = (row: Cells, column: string): Cells => {
56
+ const values = Object.values(row);
57
+ return column in row || values.length !== 1 ? row : { [column]: values[0] };
58
+ };
59
+
60
+ const scalarOf = (value: Scalar | object | undefined): Scalar | undefined =>
61
+ value === undefined || value === null || typeof value === "object" ? undefined : value;
62
+
63
+
64
+ export class CRUDRepository<T = Cells>
65
+ extends ReadonlyRepository<T>
66
+ implements ICRUDRepository<T>
67
+ {
68
+ readonly ids: readonly string[];
69
+ private readonly crudStatements: CRUDRepositoryStatements;
70
+ private readonly tree: IRepositoryTree;
71
+ private readonly write: IWriteEnrichmentStrategy<T>;
72
+ private get useTransaction(): boolean {
73
+ return this.write.useTransaction;
74
+ }
75
+
76
+ constructor(type: Constructable<T>, connection: IConnection);
77
+ constructor(connection: IConnection, options: CRUDRepositoryOptions<T>);
78
+ constructor(
79
+ typeOrConnection: Constructable<T> | IConnection,
80
+ connectionOrOptions?: IConnection | CRUDRepositoryOptions<T>,
81
+ ) {
82
+ const options = connectionOrOptions as IConnection | CRUDRepositoryOptions<T>;
83
+ const opened = repositoryParts(typeOrConnection, options);
84
+ super(opened.built);
85
+ this.ids = opened.built.ids;
86
+ this.crudStatements = opened.statements;
87
+ this.tree = new RepositoryTree(new RepositoryTreeNode(this, ""));
88
+ this.write = (options as CRUDRepositoryOptions<T>).write ?? new DefaultWriteEnrichmentStrategy<T>();
89
+ }
90
+
91
+ override addEnriched<TRelated extends object>(
92
+ enrichment: Enrichment,
93
+ via: IReadonlyRepository<TRelated>,
94
+ strategy?: IReadonlyEnrichmentStrategy<T & object>,
95
+ ): this {
96
+ super.addEnriched(enrichment, via, strategy);
97
+ const writer = writerOf(via);
98
+ if (writer) lastEnrichment(this).persist = writer;
99
+ return this;
100
+ }
101
+
102
+ addLinked<TRelated extends object, TLink extends object>(
103
+ enrichment: Enrichment,
104
+ via: IReadonlyRepository<TRelated>,
105
+ link: ICRUDRepository<TLink>,
106
+ strategy?: IReadonlyEnrichmentStrategy<T & object>,
107
+ ): this {
108
+ const chosen = strategy ?? new DefaultReadonlyEnrichmentStrategy<T & object>();
109
+ super.addEnriched(enrichment, via, strategy);
110
+ const stored = lastEnrichment(this) as StoredEnrichment<T & object>;
111
+ stored.apply = (parents, sources) =>
112
+ chosen.applyLinked(via, link, enrichment, parents, sources);
113
+ stored.link = link as ICRUDRepository<object>;
114
+ return this;
115
+ }
116
+
117
+ create = (item: T): Promise<T> => this.inTransaction(() => this.createCore(item));
118
+
119
+ update = (item: T, byWhere?: IWhere): Promise<Response> =>
120
+ this.inTransaction(() => this.updateCore(item, byWhere));
121
+
122
+ delete = (item: T, byWhere?: IWhere): Promise<Response> =>
123
+ this.inTransaction(() => this.deleteCore(item, byWhere));
124
+
125
+ private inTransaction = async <TResult>(write: () => Promise<TResult>): Promise<TResult> => {
126
+ if (!this.useTransaction) return write();
127
+ const work = new UnitOfWorkTransaction(this.command().connection);
128
+ await work.begin();
129
+ try {
130
+ const result = await write();
131
+ await work.commit();
132
+ return result;
133
+ } catch (error) {
134
+ await work.abort();
135
+ throw error;
136
+ }
137
+ };
138
+
139
+ private createCore = async (item: T): Promise<T> => {
140
+ await this.insert(item);
141
+ if (!this.enriched) return item;
142
+ await this.write.persist(this.tree, [item], "create");
143
+ if (this.ids.length !== 1) return item;
144
+ const id = this.idValue(item);
145
+ return id === undefined ? item : (await this.readSingleById(id)) ?? item;
146
+ };
147
+
148
+ private updateCore = async (item: T, byWhere?: IWhere): Promise<Response> => {
149
+ const response = await this.save(item, byWhere);
150
+ if (this.enriched) await this.write.persist(this.tree, [item], "update");
151
+ return response;
152
+ };
153
+
154
+ private deleteCore = async (item: T, byWhere?: IWhere): Promise<Response> => {
155
+ if (this.enriched) await this.write.persist(this.tree, [item], "delete");
156
+ return this.remove(item, byWhere);
157
+ };
158
+
159
+ readSingleById = async (id: Scalar | Cells): Promise<T | undefined> =>
160
+ this.readSingle(this.byId(id));
161
+
162
+ private insert = async (item: T): Promise<Response> => {
163
+ const record = this.reverseConvert(item);
164
+ const fields = fieldsOf(
165
+ record,
166
+ (col) => !this.ids.includes(col) || assigned(record[col] ?? null),
167
+ this.cell,
168
+ this.enrichmentInto,
169
+ );
170
+ const statement = this.crudStatements.insert(fields);
171
+ const response = await execute(this.command(), statement, statement.fields);
172
+ if (this.ids.length === 1 && !fields.some((field) => field.name === this.ids[0]))
173
+ await this.copyInsertedId(item, fields);
174
+ return response;
175
+ };
176
+
177
+ private copyInsertedId = async (target: T, fields: Field[]): Promise<void> => {
178
+ void fields;
179
+ const column = this.ids[0]!;
180
+ const statement = this.crudStatements.selectInsertedId(column);
181
+ const sqlColumn = statement.column;
182
+ const reader = await executeReader(this.command(), statement);
183
+ try {
184
+ if (!(await reader.next())) return;
185
+ const current = reader.current;
186
+ if (!current) throw new DatasourceError("Reader current must be a row");
187
+ const row = identified(named(current, column, sqlColumn), column);
188
+ const cells = this.rowConverter ? this.rowConverter.convert(row) : row;
189
+ const converted =
190
+ this.objectConverter instanceof MetaDataRowTypeConverter
191
+ ? this.objectConverter.convert(cells, this.type)
192
+ : this.objectConverter!.convert(cells);
193
+ const value = scalarOf((converted as Props)[column]);
194
+ if (value !== undefined) (target as Props)[column] = value;
195
+ } finally {
196
+ await reader.dispose();
197
+ }
198
+ };
199
+
200
+ private idValue = (item: T): Scalar | undefined => scalarOf((item as Props)[this.ids[0]!]);
201
+
202
+ private save = async (item: T, byWhere?: IWhere): Promise<Response> => {
203
+ const record = this.reverseConvert(item);
204
+ const fields = fieldsOf(record, (col) => !this.ids.includes(col), this.cell, this.enrichmentInto);
205
+ if (fields.length === 0) throw new DatasourceError("Update requires at least one non-Id field");
206
+ const statement = this.crudStatements.update(
207
+ fields,
208
+ byWhere ?? this.idWhere(record),
209
+ (name, value) => this.cell(name, value, byWhere === undefined),
210
+ );
211
+ return execute(this.command(), statement, statement.parameters);
212
+ };
213
+
214
+ private remove = async (item: T, byWhere?: IWhere): Promise<Response> => {
215
+ const record = this.reverseConvert(item);
216
+ const statement = this.crudStatements.delete(
217
+ byWhere ?? this.idWhere(record),
218
+ (name, value) => this.cell(name, value, byWhere === undefined),
219
+ );
220
+ const listed = statement.where?.parameters ?? [];
221
+ const parameters = listed.length
222
+ ? listed
223
+ : DefaultStatementTextRenderer.instance.render(statement).includes("$")
224
+ ? fieldsOf(record, () => true, this.cell, this.enrichmentInto)
225
+ : [];
226
+ return execute(this.command(), statement, parameters);
227
+ };
228
+
229
+ private byId = (id: Scalar | Cells): IWhere => {
230
+ if (this.ids.length === 0) throw new DatasourceError("ReadSingleById requires an Id column");
231
+ if (isCells(id)) return this.idWhere(id);
232
+ if (this.ids.length !== 1)
233
+ throw new DatasourceError("Composite id requires a row keyed by each Id column");
234
+ return this.idWhere({ [this.ids[0]!]: id });
235
+ };
236
+
237
+ private reverseConvert = (item: T): Cells => {
238
+ const row = this.objectConverter!.reverseConvert(item);
239
+ return this.rowConverter ? this.rowConverter.reverseConvert(row) : row;
240
+ };
241
+
242
+ private idWhere = (record: Cells): IWhere => {
243
+ const parts: IExpression[] = this.ids.map((col) => {
244
+ if (!Object.prototype.hasOwnProperty.call(record, col))
245
+ throw new DatasourceError(`Missing Id value "${col}"`);
246
+ return new Operator.Equals(col, record[col] as Scalar);
247
+ });
248
+ return new Where(
249
+ parts.length === 0
250
+ ? new Operator.Equals(new Literal(1), new Literal(1))
251
+ : parts.reduce((left, right) => new Operator.And(left, right)),
252
+ );
253
+ };
254
+
255
+ protected enrichmentInto = (name: string): boolean =>
256
+ enrichmentsOf(this)!.some((entry) => entry.enrichment.destinationProperty === name);
257
+ }
258
+
259
+ const writerOf = <TRelated extends object>(
260
+ via: IReadonlyRepository<TRelated>,
261
+ ): ICRUDRepository<object> | undefined =>
262
+ "create" in via && typeof via.create === "function" ? (via as ICRUDRepository<object>) : undefined;
263
+
264
+ const lastEnrichment = (host: object): StoredEnrichment => enrichmentsOf(host)!.at(-1)!;