@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
@@ -0,0 +1,97 @@
1
+ import type { Enrichment } from "./Enrichment.ts";
2
+ import type { ICRUDRepository } from "./ICRUDRepository.ts";
3
+ import type { Scalar } from "./builders/Scalar.ts";
4
+ import { defaultId } from "./createCRUDRepositoryOptions.ts";
5
+ import { DatasourceError } from "./DatasourceError.ts";
6
+ import { Operator, Where } from "./statements/index.ts";
7
+
8
+ type Props = Record<string, Scalar | object>;
9
+
10
+ export abstract class UpdateEnrichmentStrategy<T> {
11
+ abstract updateChildren<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void>;
12
+
13
+ abstract updateLinked<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void>;
14
+
15
+ protected getCell = <TItem>(item: TItem, name: string): Scalar | object | undefined =>
16
+ (item as never as Props)[name];
17
+
18
+ protected presentCell = (value: Scalar | object | undefined): value is Scalar =>
19
+ value !== undefined && value !== null && typeof value !== "object" && value !== 0 && value !== 0n && value !== "";
20
+
21
+ protected payloadOf = <TChild extends object, TParent>(
22
+ parent: TParent,
23
+ into: string,
24
+ enrichment?: Enrichment,
25
+ ): TChild[] => mapPayload(this.getCell(parent, into), enrichment) as TChild[];
26
+
27
+ protected stampCell = <TItem extends object>(related: TItem, by: string, key: Scalar | object | undefined): void => {
28
+ if (key === undefined || key === null || typeof key === "object") return;
29
+ const current = this.getCell(related, by);
30
+ if (current !== null && typeof current === "object" && !Array.isArray(current)) {
31
+ const target = current as Props;
32
+ const id = "Id" in target ? "Id" : "id";
33
+ target[id] = key;
34
+ return;
35
+ }
36
+ Object.assign(related, { [by]: key });
37
+ };
38
+
39
+ protected hasKey = <TItem extends object>(item: TItem, keys: readonly string[]): boolean =>
40
+ keys.length > 0 && keys.every((name) => this.presentCell(this.getCell(item, name)));
41
+
42
+ protected keyOf = <TItem extends object>(item: TItem, keys: readonly string[]): string =>
43
+ keys.map((name) => String(this.getCell(item, name) ?? "")).join("\u001f");
44
+
45
+ protected namesOf = <TChild extends object>(writer: ICRUDRepository<TChild>, item: TChild): readonly string[] =>
46
+ writer.ids.length > 0 ? writer.ids : defaultId(Object.keys(item));
47
+
48
+ protected linkWhere = (
49
+ association: NonNullable<Enrichment["association"]>,
50
+ parentKey: Scalar,
51
+ relatedKey: Scalar,
52
+ ): Where =>
53
+ new Where(
54
+ new Operator.And(
55
+ new Operator.Equals(association.parent, parentKey),
56
+ new Operator.Equals(association.related, relatedKey),
57
+ ),
58
+ );
59
+
60
+ protected relatedIds = <TParent>(parent: TParent, enrichment: Enrichment): Scalar[] => {
61
+ const ids: Scalar[] = [];
62
+ const seen = new Set<string>();
63
+ for (const related of this.payloadOf<object, TParent>(parent, enrichment.destinationProperty)) {
64
+ const id = this.getCell(related, enrichment.relatedKey);
65
+ if (!this.presentCell(id)) throw new DatasourceError(`${enrichment.destinationProperty} requires "${enrichment.relatedKey}"`);
66
+ const key = String(id);
67
+ if (seen.has(key)) continue;
68
+ seen.add(key);
69
+ ids.push(id);
70
+ }
71
+ return ids;
72
+ };
73
+
74
+ protected requiredKey = <TParent>(parent: TParent, enrichment: Enrichment): Scalar => {
75
+ const key = this.getCell(parent, enrichment.sourceProperty);
76
+ if (!this.presentCell(key)) throw new DatasourceError(`${enrichment.destinationProperty} requires "${enrichment.sourceProperty}"`);
77
+ return key;
78
+ };
79
+ }
80
+
81
+ const mapPayload = (value: Scalar | object | undefined, enrichment?: Enrichment): object[] => {
82
+ if (value === undefined || value === null || typeof value === "string") return [];
83
+ if (Array.isArray(value)) return value.filter((item) => item !== null && typeof item === "object");
84
+ if (enrichment?.type !== "dictionary" || typeof value !== "object") return [];
85
+ const keyName = mapKeyName(enrichment.relatedKey);
86
+ const valueName = enrichment.relatedProperty ?? (keyName === "Key" ? "Value" : "value");
87
+ return Object.entries(value as Record<string, Scalar | object>)
88
+ .sort(([left], [right]) => left.localeCompare(right))
89
+ .map(([key, cell]) =>
90
+ cell !== null && typeof cell === "object" && !Array.isArray(cell)
91
+ ? { [keyName]: key, ...(cell as object) }
92
+ : { [keyName]: key, [valueName]: cell },
93
+ );
94
+ };
95
+
96
+ const mapKeyName = (relatedKey: string): string =>
97
+ relatedKey.includes("_") || relatedKey[0] !== relatedKey[0]?.toUpperCase() ? "key" : "Key";
@@ -0,0 +1 @@
1
+ export type WriteMode = "create" | "update" | "delete";
@@ -0,0 +1,6 @@
1
+ import type { Scalar } from "./Scalar.ts";
2
+
3
+ export interface Field {
4
+ name: string;
5
+ value: Scalar;
6
+ }
@@ -0,0 +1 @@
1
+ export type Scalar = string | number | boolean | bigint | null;
@@ -0,0 +1,2 @@
1
+ export type { Field } from "./Field.ts";
2
+ export type { Scalar } from "./Scalar.ts";
@@ -0,0 +1,7 @@
1
+ export async function collectAsync<T>(items: AsyncIterable<T>): Promise<T[]> {
2
+ const collected: T[] = [];
3
+ for await (const item of items) {
4
+ collected.push(item);
5
+ }
6
+ return collected;
7
+ }
@@ -0,0 +1,10 @@
1
+ export type Cell =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | bigint
6
+ | Date
7
+ | Uint8Array
8
+ | null;
9
+
10
+ export type Cells = Record<string, Cell>;
@@ -0,0 +1,59 @@
1
+ export type Casing =
2
+ | "camelCase"
3
+ | "capitalCase"
4
+ | "constantCase"
5
+ | "dotCase"
6
+ | "kebabCase"
7
+ | "noCase"
8
+ | "pascalCase"
9
+ | "pascalSnakeCase"
10
+ | "pathCase"
11
+ | "sentenceCase"
12
+ | "snakeCase"
13
+ | "trainCase";
14
+
15
+ const cap = (word: string) => word[0].toUpperCase() + word.slice(1);
16
+ const sep = (c: string) => c === "_" || c === "." || c === "-" || c === "/" || /\s/.test(c);
17
+ const upper = (c: string) => c !== c.toLowerCase();
18
+ const lower = (c: string) => c !== c.toUpperCase();
19
+ const letter = (c: string) => upper(c) || lower(c);
20
+ const digit = (c: string) => c >= "0" && c <= "9";
21
+
22
+ const words = (text: string): string[] => {
23
+ const parts: string[] = [];
24
+ let start = 0;
25
+ for (let i = 0; i < text.length; i++) {
26
+ const split =
27
+ sep(text[i]) ||
28
+ (i > 0 && lower(text[i - 1]) && upper(text[i])) ||
29
+ (i > 0 && i + 1 < text.length && upper(text[i - 1]) && upper(text[i]) && lower(text[i + 1])) ||
30
+ (i > 0 && letter(text[i - 1]) !== letter(text[i]) && (letter(text[i - 1]) || digit(text[i - 1])) && (letter(text[i]) || digit(text[i])));
31
+ if (!split) continue;
32
+ if (i > start) parts.push(text.slice(start, i));
33
+ start = sep(text[i]) ? i + 1 : i;
34
+ }
35
+ if (start < text.length) parts.push(text.slice(start));
36
+ return parts.map((word) => word.toLowerCase()).filter((word) => word.length > 0);
37
+ };
38
+
39
+ const join = (parts: string[], casing: Casing): string =>
40
+ ({
41
+ camelCase: () => parts.map((word, i) => (i === 0 ? word : cap(word))).join(""),
42
+ capitalCase: () => parts.map(cap).join(" "),
43
+ constantCase: () => parts.map((word) => word.toUpperCase()).join("_"),
44
+ dotCase: () => parts.join("."),
45
+ kebabCase: () => parts.join("-"),
46
+ noCase: () => parts.join(" "),
47
+ pascalCase: () => parts.map(cap).join(""),
48
+ pascalSnakeCase: () => parts.map(cap).join("_"),
49
+ pathCase: () => parts.join("/"),
50
+ sentenceCase: () => parts.map((word, i) => (i === 0 ? cap(word) : word)).join(" "),
51
+ snakeCase: () => parts.join("_"),
52
+ trainCase: () => parts.map(cap).join("-"),
53
+ }[casing]());
54
+
55
+ export class ChangeCasingConverter {
56
+ constructor(private readonly casing: Casing) {}
57
+ convert = (from: string) => join(words(from), this.casing);
58
+ reverseConvert = (to: string) => to;
59
+ }
@@ -0,0 +1 @@
1
+ export type Constructable<T> = new () => T;
@@ -0,0 +1,80 @@
1
+ import type { Cell, Cells } from "./Cell.ts";
2
+ import { TypeConverter } from "./TypeConverter.ts";
3
+ import type { Constructable } from "./Constructable.ts";
4
+ import type { IMapper } from "../mappers/IMapper.ts";
5
+ import { IdentityMapper } from "../mappers/IdentityMapper.ts";
6
+
7
+ const scalarId = (value: Cell | object | undefined): Cell | undefined => {
8
+ if (value === undefined || value === null || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array)
9
+ return value === undefined ? undefined : (value as Cell);
10
+ const record = value as Record<string, Cell | undefined>;
11
+ return record.Id !== undefined ? record.Id : record.id;
12
+ };
13
+
14
+ export class MetaDataRowTypeConverter<T = Cells> extends TypeConverter<T, Cells> {
15
+ fields: Record<string, TypeConverter<Cell, Cell>>;
16
+ references = new Set<string>();
17
+ referenceColumns: IMapper<string, string> = new IdentityMapper<string>();
18
+ shouldCoerceId = false;
19
+
20
+ constructor(fields: Record<string, TypeConverter<Cell, Cell>> = {}) {
21
+ super();
22
+ this.fields = { ...fields };
23
+ }
24
+
25
+ convert = (row: Cells, type?: Constructable<T>): T => {
26
+ const blank = type ? new type() : {};
27
+ const cells = Object.fromEntries(
28
+ Object.entries(row).filter(([key, value]) => {
29
+ if (this.references.has(key)) return false;
30
+ const current = (blank as Cells)[key];
31
+ return !(typeof current === "string" && typeof value !== "string" && value !== null);
32
+ }),
33
+ );
34
+ const dest = Object.assign(blank, cells) as T;
35
+ const record = dest as Record<string, Cell | object>;
36
+ for (const [key, value] of Object.entries(row)) {
37
+ if (this.references.has(key)) {
38
+ const current = record[key];
39
+ const target = current !== null && typeof current === "object" && !Array.isArray(current)
40
+ ? current as Record<string, Cell>
41
+ : {};
42
+ const id = "Id" in target ? "Id" : "id";
43
+ target[id] = value;
44
+ record[key] = target;
45
+ continue;
46
+ }
47
+ const converter = this.fields[key];
48
+ if (converter) record[key] = converter.convert(value);
49
+ }
50
+ return dest;
51
+ };
52
+
53
+ reverseConvert = (row: T): Cells => {
54
+ const cells: Cells = {};
55
+ for (const [key, value] of Object.entries(row as Record<string, Cell | object>)) {
56
+ if (Array.isArray(value)) continue;
57
+ if (
58
+ value !== null &&
59
+ typeof value === "object" &&
60
+ !(value instanceof Date) &&
61
+ !(value instanceof Uint8Array) &&
62
+ !this.references.has(key)
63
+ )
64
+ continue;
65
+ if (this.references.has(key)) {
66
+ const id = scalarId(value);
67
+ if (id !== undefined && id !== null) cells[this.referenceColumns.map(key)] = this.converted(key, id);
68
+ continue;
69
+ }
70
+ const converter = this.fields[key];
71
+ cells[key] = converter ? converter.reverseConvert(value as Cell) : (value as Cell);
72
+ }
73
+ return cells;
74
+ };
75
+
76
+ private converted = (key: string, value: Cell): Cell => {
77
+ const converter = this.fields[key];
78
+ return converter ? converter.reverseConvert(value) : value;
79
+ };
80
+ }
@@ -0,0 +1,67 @@
1
+ const uncountable = new Set(["sheep", "fish", "series", "species", "deer", "news"]);
2
+ const irregular: Record<string, string> = {
3
+ person: "people",
4
+ child: "children",
5
+ man: "men",
6
+ woman: "women",
7
+ mouse: "mice",
8
+ goose: "geese",
9
+ tooth: "teeth",
10
+ foot: "feet",
11
+ ox: "oxen",
12
+ };
13
+ const irregularSingular = Object.fromEntries(
14
+ Object.entries(irregular).map(([singular, plural]) => [plural, singular]),
15
+ );
16
+
17
+ const vowel = (c: string) => "aeiou".includes(c);
18
+ const consonant = (c: string) => /[a-z]/.test(c) && !vowel(c);
19
+ const yies = (word: string) => word.length >= 4 && word.endsWith("ies") && consonant(word[word.length - 4]);
20
+ const consonantY = (word: string) => word.length >= 2 && word.endsWith("y") && consonant(word[word.length - 2]);
21
+ const sibilant = (word: string) =>
22
+ word.endsWith("ch") ||
23
+ word.endsWith("sh") ||
24
+ word.endsWith("x") ||
25
+ word.endsWith("z") ||
26
+ word.endsWith("ss") ||
27
+ (word.length >= 2 && word.endsWith("s") && !word.endsWith("es") && vowel(word[word.length - 2]));
28
+ const sibilantEs = (word: string) => word.length > 2 && word.endsWith("es") && sibilant(word.slice(0, -2));
29
+
30
+ const restore = (original: string, result: string) =>
31
+ original[0] === original[0].toUpperCase()
32
+ ? result[0].toUpperCase() + result.slice(1)
33
+ : result;
34
+
35
+ const plural = (word: string) =>
36
+ uncountable.has(word) || word in irregularSingular
37
+ ? word
38
+ : irregular[word] ??
39
+ (yies(word) || sibilantEs(word)
40
+ ? word
41
+ : sibilant(word)
42
+ ? word + "es"
43
+ : consonantY(word)
44
+ ? word.slice(0, -1) + "ies"
45
+ : word.endsWith("s")
46
+ ? word
47
+ : word + "s");
48
+
49
+ const singular = (word: string) =>
50
+ uncountable.has(word) || word in irregular
51
+ ? word
52
+ : (irregularSingular[word] ??
53
+ (yies(word)
54
+ ? word.slice(0, -3) + "y"
55
+ : sibilantEs(word)
56
+ ? word.slice(0, -2)
57
+ : word.endsWith("s") && !sibilant(word)
58
+ ? word.slice(0, -1)
59
+ : word));
60
+
61
+ const apply = (word: string, transform: (word: string) => string) =>
62
+ word.length === 0 ? word : restore(word, transform(word.toLowerCase()));
63
+
64
+ export class PluralizerConverter {
65
+ convert = (from: string) => apply(from, plural);
66
+ reverseConvert = (to: string) => apply(to, singular);
67
+ }
@@ -0,0 +1,14 @@
1
+ export class RegexConverter {
2
+ readonly regex: RegExp;
3
+ readonly replacement: string;
4
+
5
+ constructor(pattern: string | RegExp, replacement: string) {
6
+ this.regex = typeof pattern === "string"
7
+ ? new RegExp(pattern, "g")
8
+ : new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
9
+ this.replacement = replacement;
10
+ }
11
+
12
+ convert = (from: string) => from.replace(this.regex, this.replacement);
13
+ reverseConvert = (to: string) => to;
14
+ }
@@ -0,0 +1,6 @@
1
+ export class ReplaceConverter {
2
+ constructor(readonly search: string, readonly replacement: string) {}
3
+
4
+ convert = (from: string) => from.replaceAll(this.search, this.replacement);
5
+ reverseConvert = (to: string) => to.replaceAll(this.replacement, this.search);
6
+ }
@@ -0,0 +1,33 @@
1
+ import type { Cell, Cells } from "./Cell.ts";
2
+ import { TypeConverter } from "./TypeConverter.ts";
3
+
4
+ export class RowTypeConverter extends TypeConverter<Cells, Cells> {
5
+ constructor(readonly fields: Record<string, TypeConverter<Cell, Cell>>) {
6
+ super();
7
+ }
8
+
9
+ static from(
10
+ converters: Record<string, TypeConverter<Cell, Cell>>,
11
+ columns: Record<string, string>,
12
+ ): RowTypeConverter {
13
+ const fields: Record<string, TypeConverter<Cell, Cell>> = {};
14
+ for (const [name, type] of Object.entries(columns)) {
15
+ const converter = converters[type];
16
+ if (converter) fields[name] = converter;
17
+ }
18
+ return new RowTypeConverter(fields);
19
+ }
20
+
21
+ convert = (from: Cells): Cells => this.apply(from, false);
22
+
23
+ reverseConvert = (to: Cells): Cells => this.apply(to, true);
24
+
25
+ private apply(row: Cells, reverse: boolean): Cells {
26
+ return Object.fromEntries(
27
+ Object.entries(row).map(([name, value]) => {
28
+ const converter = this.fields[name];
29
+ return [name, converter ? (reverse ? converter.reverseConvert(value) : converter.convert(value)) : value];
30
+ }),
31
+ );
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ export class ToLowerCaseConverter {
2
+ convert = (from: string) => from.toLowerCase();
3
+ reverseConvert = (to: string) => to;
4
+ }
@@ -0,0 +1,4 @@
1
+ export class ToUpperCaseConverter {
2
+ convert = (from: string) => from.toUpperCase();
3
+ reverseConvert = (to: string) => to;
4
+ }
@@ -0,0 +1,6 @@
1
+ import type { Cells } from "./Cell.ts";
2
+
3
+ export abstract class TypeConverter<To = Cells, From = Cells> {
4
+ abstract convert(from: From): To;
5
+ abstract reverseConvert(to: To): From;
6
+ }
@@ -0,0 +1,21 @@
1
+ import type { Cell } from "./converters/Cell.ts";
2
+ import type { Constructable } from "./converters/Constructable.ts";
3
+ import type { TypeConverter } from "./converters/TypeConverter.ts";
4
+ import type { CRUDRepositoryOptions } from "./CRUDRepositoryOptions.ts";
5
+ import {
6
+ createReadonlyRepositoryMappingOptions,
7
+ fieldNamesOf,
8
+ } from "./createReadonlyRepositoryMappingOptions.ts";
9
+
10
+ export { fieldNamesOf } from "./createReadonlyRepositoryMappingOptions.ts";
11
+
12
+ export const defaultId = (fields: string[]): string[] =>
13
+ fields.includes("id") ? ["id"] : fields.includes("Id") ? ["Id"] : [];
14
+
15
+ export const createCRUDRepositoryOptions = <T>(
16
+ type: Constructable<T>,
17
+ rowTypeConverters?: Record<string, TypeConverter<Cell, Cell>>,
18
+ ): CRUDRepositoryOptions<T> => ({
19
+ ...createReadonlyRepositoryMappingOptions(type, rowTypeConverters),
20
+ ids: defaultId(fieldNamesOf(type)),
21
+ });
@@ -0,0 +1,26 @@
1
+ import type { Cell } from "./converters/Cell.ts";
2
+ import type { Constructable } from "./converters/Constructable.ts";
3
+ import type { TypeConverter } from "./converters/TypeConverter.ts";
4
+ import type { ReadonlyRepositoryMappingOptions } from "./ReadonlyRepositoryMappingOptions.ts";
5
+
6
+ type FieldSource = Record<string, string | number | boolean | bigint | null | object>;
7
+
8
+ export const fieldNamesOf = <T>(type: Constructable<T>): string[] => {
9
+ const instance = new type() as FieldSource;
10
+ return Object.keys(instance).filter((key) => {
11
+ const value = instance[key];
12
+ if (Array.isArray(value)) return false;
13
+ if (value !== null && typeof value === "object" && !(value instanceof Date) && !(value instanceof Uint8Array))
14
+ return "id" in value || "Id" in value;
15
+ return true;
16
+ });
17
+ };
18
+
19
+ export const createReadonlyRepositoryMappingOptions = <T>(
20
+ type: Constructable<T>,
21
+ rowTypeConverters?: Record<string, TypeConverter<Cell, Cell>>,
22
+ ): ReadonlyRepositoryMappingOptions<T> => ({
23
+ type,
24
+ entity: type.name,
25
+ rowTypeConverters,
26
+ });
package/src/dotEnv.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import type { DatasourceOptions } from "./DatasourceOptions.ts";
3
+
4
+ export const DotEnv = {
5
+ parse: (text: string): DatasourceOptions =>
6
+ Object.fromEntries(
7
+ text
8
+ .split(/\r?\n/)
9
+ .map((raw) => raw.trimStart().split("=", 2))
10
+ .filter((parts): parts is [string, string] =>
11
+ parts.length === 2 && !parts[0].startsWith("#") && parts[0].trim() !== "")
12
+ .map(([key, value]) => [key.trim(), value]),
13
+ ),
14
+ load: (path = ".env"): DatasourceOptions =>
15
+ existsSync(path) ? DotEnv.parse(readFileSync(path, "utf8")) : {},
16
+ };