@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.
- package/package.json +16 -4
- package/src/ArrayReader.ts +32 -0
- package/src/BatchCommandIBulkWriteStrategy.ts +228 -0
- package/src/BuiltRepository.ts +17 -0
- package/src/BulkCRUDRepository.ts +84 -0
- package/src/BulkCommand.ts +6 -0
- package/src/BulkWriteEnrichment.ts +19 -0
- package/src/CRUDRepository.ts +264 -0
- package/src/CRUDRepositoryBuilder.ts +53 -0
- package/src/CRUDRepositoryOptions.ts +8 -0
- package/src/CRUDRepositoryStatements.ts +43 -0
- package/src/CascadeDeleteEnrichmentStrategy.ts +51 -0
- package/src/CascadeDeleteUpdateEnrichmentStrategy.ts +28 -0
- package/src/CommandMiddlewareHost.ts +113 -0
- package/src/ConsoleCommandMiddleware.ts +64 -0
- package/src/CreateEnrichmentStrategy.ts +82 -0
- package/src/Datasource.ts +152 -0
- package/src/DatasourceError.ts +6 -0
- package/src/DatasourceFactory.ts +86 -0
- package/src/DatasourceOptions.ts +1 -0
- package/src/DatasourceUrl.ts +37 -0
- package/src/DatasourceUrlParser.ts +188 -0
- package/src/DefaultCreateEnrichmentStrategy.ts +21 -0
- package/src/DefaultReadonlyEnrichmentStrategy.ts +164 -0
- package/src/DefaultUpdateEnrichmentStrategy.ts +40 -0
- package/src/DefaultWriteEnrichmentStrategy.ts +151 -0
- package/src/DeleteEnrichmentStrategy.ts +56 -0
- package/src/DriverLoader.ts +97 -0
- package/src/Enrichment.ts +15 -0
- package/src/EnrichmentPropertyType.ts +9 -0
- package/src/ExpectConsoleMiddleware.ts +47 -0
- package/src/FirstResultReader.ts +49 -0
- package/src/HashsetOptimizedReadonlyEnrichmentStrategy.ts +80 -0
- package/src/IBulkWriteStrategy.ts +13 -0
- package/src/ICRUDRepository.ts +13 -0
- package/src/ICRUDRepositoryFactory.ts +8 -0
- package/src/ICommand.ts +45 -0
- package/src/ICommandMiddleware.ts +20 -0
- package/src/IConnection.ts +13 -0
- package/src/IDatasource.ts +27 -0
- package/src/IDatasourceFactory.ts +6 -0
- package/src/IDriver.ts +24 -0
- package/src/IEnriched.ts +11 -0
- package/src/IReader.ts +8 -0
- package/src/IReadonlyEnrichmentStrategy.ts +19 -0
- package/src/IReadonlyRepository.ts +21 -0
- package/src/IRepository.ts +5 -0
- package/src/IRepositoryFactory.ts +8 -0
- package/src/IRepositoryTree.ts +5 -0
- package/src/IRepositoryTreeNode.ts +8 -0
- package/src/ITransaction.ts +9 -0
- package/src/IUnitOfWork.ts +5 -0
- package/src/IWriteEnrichmentStrategy.ts +8 -0
- package/src/Information.ts +5 -0
- package/src/IsolationLevel.ts +8 -0
- package/src/LazyWriteEnrichmentStrategy.ts +5 -0
- package/src/NestedToObjectEnrichmentStrategy.ts +33 -0
- package/src/Parameter.ts +8 -0
- package/src/ParameterDirection.ts +9 -0
- package/src/ProviderCatalog.ts +21 -0
- package/src/ProviderNotImplementedError.ts +9 -0
- package/src/ProviderNotInstalledError.ts +9 -0
- package/src/ReadonlyRepository.ts +276 -0
- package/src/ReadonlyRepositoryBuilder.ts +53 -0
- package/src/ReadonlyRepositoryMappingOptions.ts +15 -0
- package/src/ReadonlyRepositoryStatements.ts +13 -0
- package/src/RepositoryAccess.ts +14 -0
- package/src/RepositoryFactory.ts +115 -0
- package/src/RepositoryNotRegisteredError.ts +9 -0
- package/src/RepositoryTree.ts +6 -0
- package/src/RepositoryTreeNode.ts +11 -0
- package/src/Response.ts +15 -0
- package/src/Row.ts +3 -0
- package/src/SingleCommandIBulkWriteStrategy.ts +35 -0
- package/src/StatementMappers.ts +110 -0
- package/src/StoredEnrichment.ts +25 -0
- package/src/StoredProcedureBulkWriteStrategy.ts +164 -0
- package/src/StreamingReader.ts +64 -0
- package/src/UnitOfWorkTransaction.ts +50 -0
- package/src/UpdateEnrichmentStrategy.ts +97 -0
- package/src/WriteMode.ts +1 -0
- package/src/builders/Field.ts +6 -0
- package/src/builders/Scalar.ts +1 -0
- package/src/builders/index.ts +2 -0
- package/src/collectAsync.ts +7 -0
- package/src/converters/Cell.ts +10 -0
- package/src/converters/ChangeCasingConverter.ts +59 -0
- package/src/converters/Constructable.ts +1 -0
- package/src/converters/MetaDataRowTypeConverter.ts +80 -0
- package/src/converters/PluralizerConverter.ts +67 -0
- package/src/converters/RegexConverter.ts +14 -0
- package/src/converters/ReplaceConverter.ts +6 -0
- package/src/converters/RowTypeConverter.ts +33 -0
- package/src/converters/ToLowerCaseConverter.ts +4 -0
- package/src/converters/ToUpperCaseConverter.ts +4 -0
- package/src/converters/TypeConverter.ts +6 -0
- package/src/createCRUDRepositoryOptions.ts +21 -0
- package/src/createReadonlyRepositoryMappingOptions.ts +26 -0
- package/src/dotEnv.ts +16 -0
- package/src/index.ts +222 -0
- package/src/mappers/ChangeCasingMapper.ts +15 -0
- package/src/mappers/DictionaryMapper.ts +16 -0
- package/src/mappers/FlatToNestedMapper.ts +127 -0
- package/src/mappers/IFlatToNestedMapper.ts +7 -0
- package/src/mappers/IMapper.ts +4 -0
- package/src/mappers/IdentityMapper.ts +6 -0
- package/src/mappers/PluralizerMapper.ts +9 -0
- package/src/mappers/RowTypeMapper.ts +13 -0
- package/src/mappers/TransformConverterMapper.ts +11 -0
- package/src/mappers/TransformMapper.ts +8 -0
- package/src/mappers/TypeMap.ts +10 -0
- package/src/mappers/TypeMapGroup.ts +6 -0
- package/src/mocks/index.ts +9 -0
- package/src/mocks/mock-command.ts +66 -0
- package/src/mocks/mock-connection.ts +57 -0
- package/src/mocks/mock-datasource.ts +65 -0
- package/src/mocks/mock-driver.ts +27 -0
- package/src/mocks/mock-reader.ts +38 -0
- package/src/mocks/mock-repository.ts +91 -0
- package/src/mocks/mock-select-statement.ts +17 -0
- package/src/mocks/mock-transaction.ts +31 -0
- package/src/mocks/mock-where.ts +13 -0
- package/src/parametersToRecord.ts +11 -0
- package/src/readerToAsyncIterable.ts +11 -0
- package/src/splitStatements.ts +99 -0
- package/src/statementParameters.ts +22 -0
- package/src/statementUsesParameter.ts +4 -0
- package/src/statements/Aggregate.ts +9 -0
- package/src/statements/ArrayExpression.ts +5 -0
- package/src/statements/BulkDeleteStatement.ts +18 -0
- package/src/statements/BulkDeleteStatementTextRenderer.ts +16 -0
- package/src/statements/BulkInsertStatement.ts +37 -0
- package/src/statements/BulkInsertStatementTextRenderer.ts +10 -0
- package/src/statements/BulkUpdateStatement.ts +24 -0
- package/src/statements/BulkUpdateStatementTextRenderer.ts +29 -0
- package/src/statements/CheckConstraint.ts +9 -0
- package/src/statements/Cmp.ts +12 -0
- package/src/statements/Column.ts +10 -0
- package/src/statements/ColumnDefault.ts +6 -0
- package/src/statements/Condition.ts +3 -0
- package/src/statements/ConditionRenderer.ts +17 -0
- package/src/statements/CreateIndexStatement.ts +18 -0
- package/src/statements/CreateIndexStatementTextRenderer.ts +11 -0
- package/src/statements/CreateTableStatement.ts +18 -0
- package/src/statements/CreateTableStatementTextRenderer.ts +92 -0
- package/src/statements/DefaultStatementTextRenderer.ts +9 -0
- package/src/statements/DeleteStatement.ts +15 -0
- package/src/statements/DeleteStatementTextRenderer.ts +12 -0
- package/src/statements/ExecuteStoredProcedureStatement.ts +15 -0
- package/src/statements/ExecuteStoredProcedureStatementTextRenderer.ts +15 -0
- package/src/statements/ExpressionEvaluator.ts +166 -0
- package/src/statements/ExpressionRenderer.ts +161 -0
- package/src/statements/ForeignKey.ts +23 -0
- package/src/statements/IBulkDeleteStatement.ts +3 -0
- package/src/statements/IBulkInsertStatement.ts +3 -0
- package/src/statements/IBulkUpdateStatement.ts +3 -0
- package/src/statements/ICondition.ts +5 -0
- package/src/statements/IConditionVisitor.ts +7 -0
- package/src/statements/ICreateIndexStatement.ts +3 -0
- package/src/statements/ICreateTableStatement.ts +3 -0
- package/src/statements/IDeleteStatement.ts +6 -0
- package/src/statements/IExecuteStoredProcedureStatement.ts +3 -0
- package/src/statements/IExpression.ts +1 -0
- package/src/statements/IInsertStatement.ts +3 -0
- package/src/statements/IReturnStatement.ts +3 -0
- package/src/statements/ISelectStatement.ts +6 -0
- package/src/statements/IStatement.ts +5 -0
- package/src/statements/IStatementVisitor.ts +27 -0
- package/src/statements/ITableConstraint.ts +6 -0
- package/src/statements/IUpdateStatement.ts +6 -0
- package/src/statements/IWhere.ts +7 -0
- package/src/statements/Identifier.ts +5 -0
- package/src/statements/InsertStatement.ts +15 -0
- package/src/statements/InsertStatementTextRenderer.ts +7 -0
- package/src/statements/Join.ts +26 -0
- package/src/statements/Literal.ts +6 -0
- package/src/statements/Native.ts +5 -0
- package/src/statements/NotNull.ts +3 -0
- package/src/statements/Operator.ts +234 -0
- package/src/statements/OrderBy.ts +19 -0
- package/src/statements/PrimaryKey.ts +9 -0
- package/src/statements/ReturnStatement.ts +15 -0
- package/src/statements/ReturnStatementTextRenderer.ts +15 -0
- package/src/statements/SelectInsertedIdStatement.ts +11 -0
- package/src/statements/SelectInsertedIdStatementTextRenderer.ts +7 -0
- package/src/statements/SelectStatement.ts +34 -0
- package/src/statements/SelectStatementOptions.ts +16 -0
- package/src/statements/SelectStatementTextRenderer.ts +71 -0
- package/src/statements/StatementEvaluator.ts +5 -0
- package/src/statements/StatementParser.ts +461 -0
- package/src/statements/StatementRenderer.ts +120 -0
- package/src/statements/StatementTextRenderer.ts +60 -0
- package/src/statements/Subquery.ts +6 -0
- package/src/statements/UniqueConstraint.ts +9 -0
- package/src/statements/UpdateStatement.ts +34 -0
- package/src/statements/UpdateStatementTextRenderer.ts +63 -0
- package/src/statements/Where.ts +250 -0
- package/src/statements/WhereText.ts +10 -0
- package/src/statements/index.ts +75 -0
- package/src/stubs/StubCommand.ts +30 -0
- package/src/stubs/StubConnection.ts +21 -0
- package/src/stubs/StubDatasource.ts +60 -0
- package/src/stubs/StubReader.ts +25 -0
- package/src/stubs/StubRepository.ts +42 -0
- package/src/stubs/StubTransaction.ts +17 -0
- package/src/stubs/createStubDatasource.ts +4 -0
- package/src/stubs/index.ts +7 -0
- package/src/stubs/notImplemented.ts +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { IDatasource } from "./IDatasource.ts";
|
|
2
|
+
import type { IDatasourceFactory } from "./IDatasourceFactory.ts";
|
|
3
|
+
import type { IDriver } from "./IDriver.ts";
|
|
4
|
+
import type { DatasourceOptions } from "./DatasourceOptions.ts";
|
|
5
|
+
import { Datasource } from "./Datasource.ts";
|
|
6
|
+
import { DatasourceError } from "./DatasourceError.ts";
|
|
7
|
+
import { DatasourceUrlParser } from "./DatasourceUrlParser.ts";
|
|
8
|
+
import { DriverLoader } from "./DriverLoader.ts";
|
|
9
|
+
import { DotEnv } from "./dotEnv.ts";
|
|
10
|
+
import { ProviderCatalog } from "./ProviderCatalog.ts";
|
|
11
|
+
import { ProviderNotInstalledError } from "./ProviderNotInstalledError.ts";
|
|
12
|
+
|
|
13
|
+
const factories = new Map<string, IDriver>();
|
|
14
|
+
|
|
15
|
+
const get = (options: DatasourceOptions | undefined, key: string): string | undefined => {
|
|
16
|
+
const value = options?.[key];
|
|
17
|
+
return value && value.trim() ? value.trim() : undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const nonEmpty = (value: string | undefined): string | undefined =>
|
|
21
|
+
value?.trim() ? value.trim() : undefined;
|
|
22
|
+
|
|
23
|
+
const merge = (file: DatasourceOptions, env: Record<string, string | undefined>): DatasourceOptions => {
|
|
24
|
+
const options: DatasourceOptions = { ...file };
|
|
25
|
+
for (const key of ["INSTALLED_PROVIDERS", "DATASOURCE_URL", "PROVIDER"] as const) {
|
|
26
|
+
const value = nonEmpty(env[key]);
|
|
27
|
+
if (value) options[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return options;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const split = (value: string | undefined): string[] =>
|
|
33
|
+
(value ?? "")
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((name) => name.trim())
|
|
36
|
+
.filter((name) => name.length > 0);
|
|
37
|
+
|
|
38
|
+
function* installed(options: DatasourceOptions): Generator<string> {
|
|
39
|
+
const names = new Set<string>();
|
|
40
|
+
for (const name of split(get(options, "INSTALLED_PROVIDERS"))) {
|
|
41
|
+
if (names.has(name)) continue;
|
|
42
|
+
names.add(name);
|
|
43
|
+
yield name;
|
|
44
|
+
}
|
|
45
|
+
const url = get(options, "DATASOURCE_URL");
|
|
46
|
+
if (!url) return;
|
|
47
|
+
const provider = DatasourceUrlParser.parse(url)?.provider;
|
|
48
|
+
if (!provider) throw new DatasourceError("DATASOURCE_URL has no provider");
|
|
49
|
+
if (!names.has(provider)) yield provider;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const registerInstalled = (options: DatasourceOptions): void => {
|
|
53
|
+
for (const name of installed(options)) {
|
|
54
|
+
const catalog = ProviderCatalog.default[name];
|
|
55
|
+
if (!catalog) throw new ProviderNotInstalledError(name);
|
|
56
|
+
DatasourceFactory.register(name, DriverLoader.load(catalog));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const resolve = (options?: DatasourceOptions): IDatasource => {
|
|
61
|
+
const listed = options ?? merge(DotEnv.load(), process.env);
|
|
62
|
+
if (options === undefined) registerInstalled(listed);
|
|
63
|
+
const name = get(listed, "PROVIDER")
|
|
64
|
+
?? (() => { throw new DatasourceError("Datasource defaults are not specified"); })();
|
|
65
|
+
const driver = factories.get(name);
|
|
66
|
+
if (!driver) throw new ProviderNotInstalledError(name);
|
|
67
|
+
return new Datasource(name, driver, listed);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class DatasourceFactory implements IDatasourceFactory {
|
|
71
|
+
static defaults = (options: DatasourceOptions): void => registerInstalled(options);
|
|
72
|
+
|
|
73
|
+
static register = (provider: string, driver: IDriver): void => {
|
|
74
|
+
factories.set(provider, driver);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
static create = (options?: DatasourceOptions): IDatasource => resolve(options);
|
|
78
|
+
|
|
79
|
+
create = (options?: DatasourceOptions): IDatasource => resolve(options);
|
|
80
|
+
|
|
81
|
+
static reset = (): void => {
|
|
82
|
+
factories.clear();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
static registered = (): string[] => [...factories.keys()].sort();
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type DatasourceOptions = Record<string, string>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class DatasourceUrl {
|
|
2
|
+
readonly provider: string;
|
|
3
|
+
readonly username: string | undefined;
|
|
4
|
+
readonly password: string | undefined;
|
|
5
|
+
readonly host: string | undefined;
|
|
6
|
+
readonly port: number | undefined;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly schema: string | undefined;
|
|
9
|
+
readonly options: Readonly<Record<string, string>>;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
provider: string,
|
|
13
|
+
username: string | undefined,
|
|
14
|
+
password: string | undefined,
|
|
15
|
+
host: string | undefined,
|
|
16
|
+
port: number | undefined,
|
|
17
|
+
path: string,
|
|
18
|
+
schema: string | undefined,
|
|
19
|
+
options: Readonly<Record<string, string>>,
|
|
20
|
+
) {
|
|
21
|
+
this.provider = provider;
|
|
22
|
+
this.username = username;
|
|
23
|
+
this.password = password;
|
|
24
|
+
this.host = host;
|
|
25
|
+
this.port = port;
|
|
26
|
+
this.path = path;
|
|
27
|
+
this.schema = schema;
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get datasource(): string | undefined {
|
|
32
|
+
if (this.path === "") return undefined;
|
|
33
|
+
if (!this.path.startsWith("/")) return this.path;
|
|
34
|
+
const rest = this.path.slice(1);
|
|
35
|
+
return rest === "" ? undefined : rest;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { DatasourceUrl } from "./DatasourceUrl.ts";
|
|
2
|
+
|
|
3
|
+
const empty: Readonly<Record<string, string>> = {};
|
|
4
|
+
|
|
5
|
+
const schemeStart = (c: string): boolean =>
|
|
6
|
+
(c >= "A" && c <= "Z") || (c >= "a" && c <= "z");
|
|
7
|
+
|
|
8
|
+
const schemeChar = (c: string): boolean =>
|
|
9
|
+
schemeStart(c) || (c >= "0" && c <= "9") || c === "+" || c === "." || c === "-";
|
|
10
|
+
|
|
11
|
+
const schemeColon = (url: string): number | undefined => {
|
|
12
|
+
if (!schemeStart(url.charAt(0))) return undefined;
|
|
13
|
+
let i = 1;
|
|
14
|
+
while (i < url.length && schemeChar(url[i])) i++;
|
|
15
|
+
return i < url.length && url[i] === ":" ? i : undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const hex = (text: string): number | undefined =>
|
|
19
|
+
/^[0-9a-fA-F]{2}$/.test(text) ? Number.parseInt(text, 16) : undefined;
|
|
20
|
+
|
|
21
|
+
const decode = (text: string): string | undefined => {
|
|
22
|
+
const bytes: number[] = [];
|
|
23
|
+
let decoded = "";
|
|
24
|
+
const flush = (): void => {
|
|
25
|
+
if (bytes.length === 0) return;
|
|
26
|
+
decoded += new TextDecoder().decode(new Uint8Array(bytes));
|
|
27
|
+
bytes.length = 0;
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
const c = text[i];
|
|
31
|
+
if (c !== "%") {
|
|
32
|
+
const code = text.charCodeAt(i);
|
|
33
|
+
if (code <= 127) bytes.push(code);
|
|
34
|
+
else {
|
|
35
|
+
flush();
|
|
36
|
+
decoded += c;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const byte = i + 2 < text.length ? hex(text.slice(i + 1, i + 3)) : undefined;
|
|
41
|
+
if (byte === undefined) return undefined;
|
|
42
|
+
bytes.push(byte);
|
|
43
|
+
i += 2;
|
|
44
|
+
}
|
|
45
|
+
flush();
|
|
46
|
+
return decoded;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const portOf = (text: string): number | undefined =>
|
|
50
|
+
/^[0-9]+$/.test(text) ? Number.parseInt(text, 10) : undefined;
|
|
51
|
+
|
|
52
|
+
type Authority = {
|
|
53
|
+
username: string | undefined;
|
|
54
|
+
password: string | undefined;
|
|
55
|
+
host: string | undefined;
|
|
56
|
+
port: number | undefined;
|
|
57
|
+
file: string | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const userOf = (user: string): { username: string | undefined; password: string | undefined } | undefined => {
|
|
61
|
+
const colon = user.indexOf(":");
|
|
62
|
+
const username = decode(colon < 0 ? user : user.slice(0, colon));
|
|
63
|
+
if (username === undefined) return undefined;
|
|
64
|
+
if (colon < 0) return { username: username === "" ? undefined : username, password: undefined };
|
|
65
|
+
const password = decode(user.slice(colon + 1));
|
|
66
|
+
if (password === undefined) return undefined;
|
|
67
|
+
return { username: username === "" ? undefined : username, password };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const hostLiteral = (hostport: string): { host: string | undefined; port: number | undefined } | undefined => {
|
|
71
|
+
const end = hostport.indexOf("]");
|
|
72
|
+
if (end < 1) return undefined;
|
|
73
|
+
const host = decode(hostport.slice(1, end));
|
|
74
|
+
if (host === undefined) return undefined;
|
|
75
|
+
if (end + 1 === hostport.length) return { host, port: undefined };
|
|
76
|
+
if (hostport[end + 1] !== ":") return undefined;
|
|
77
|
+
const port = portOf(hostport.slice(end + 2));
|
|
78
|
+
return port === undefined ? undefined : { host, port };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const authorityOf = (authority: string, hasPath: boolean): Authority | undefined => {
|
|
82
|
+
if (authority.length === 0)
|
|
83
|
+
return { username: undefined, password: undefined, host: undefined, port: undefined, file: undefined };
|
|
84
|
+
const at = authority.lastIndexOf("@");
|
|
85
|
+
let hostport = authority;
|
|
86
|
+
let username: string | undefined;
|
|
87
|
+
let password: string | undefined;
|
|
88
|
+
if (at >= 0) {
|
|
89
|
+
const user = userOf(authority.slice(0, at));
|
|
90
|
+
if (!user) return undefined;
|
|
91
|
+
username = user.username;
|
|
92
|
+
password = user.password;
|
|
93
|
+
hostport = authority.slice(at + 1);
|
|
94
|
+
}
|
|
95
|
+
const base = { username, password, file: undefined };
|
|
96
|
+
if (hostport.length === 0) return { ...base, host: undefined, port: undefined };
|
|
97
|
+
if (hostport[0] === "[") {
|
|
98
|
+
const literal = hostLiteral(hostport);
|
|
99
|
+
return literal ? { ...base, host: literal.host, port: literal.port } : undefined;
|
|
100
|
+
}
|
|
101
|
+
const colon = hostport.lastIndexOf(":");
|
|
102
|
+
if (colon >= 0) {
|
|
103
|
+
const port = portOf(hostport.slice(colon + 1));
|
|
104
|
+
if (port !== undefined) {
|
|
105
|
+
const name = hostport.slice(0, colon);
|
|
106
|
+
if (name.length === 0) return { ...base, host: undefined, port };
|
|
107
|
+
const host = decode(name);
|
|
108
|
+
return host === undefined ? undefined : { ...base, host, port };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!hasPath && at < 0 && hostport[0] !== ":") {
|
|
112
|
+
const file = decode(hostport);
|
|
113
|
+
return file === undefined ? undefined : { ...base, host: undefined, port: undefined, file };
|
|
114
|
+
}
|
|
115
|
+
const host = decode(hostport);
|
|
116
|
+
return host === undefined ? undefined : { ...base, host, port: undefined };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const queryOf = (
|
|
120
|
+
query: string,
|
|
121
|
+
): { schema: string | undefined; options: Readonly<Record<string, string>> } | undefined => {
|
|
122
|
+
if (query.length === 0) return { schema: undefined, options: empty };
|
|
123
|
+
const options: Record<string, string> = {};
|
|
124
|
+
let schema: string | undefined;
|
|
125
|
+
for (const part of query.split("&")) {
|
|
126
|
+
if (part.length === 0) continue;
|
|
127
|
+
const eq = part.indexOf("=");
|
|
128
|
+
const key = decode(eq < 0 ? part : part.slice(0, eq));
|
|
129
|
+
const value = decode(eq < 0 ? "" : part.slice(eq + 1));
|
|
130
|
+
if (key === undefined || value === undefined) return undefined;
|
|
131
|
+
if (key === "schema") schema = value;
|
|
132
|
+
else options[key] = value;
|
|
133
|
+
}
|
|
134
|
+
return { schema, options };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const partsOf = (
|
|
138
|
+
rest: string,
|
|
139
|
+
):
|
|
140
|
+
| {
|
|
141
|
+
username: string | undefined;
|
|
142
|
+
password: string | undefined;
|
|
143
|
+
host: string | undefined;
|
|
144
|
+
port: number | undefined;
|
|
145
|
+
path: string;
|
|
146
|
+
}
|
|
147
|
+
| undefined => {
|
|
148
|
+
if (!rest.startsWith("//"))
|
|
149
|
+
return { username: undefined, password: undefined, host: undefined, port: undefined, path: rest };
|
|
150
|
+
const body = rest.slice(2);
|
|
151
|
+
const slash = body.indexOf("/");
|
|
152
|
+
const authority = authorityOf(slash >= 0 ? body.slice(0, slash) : body, slash >= 0);
|
|
153
|
+
if (!authority) return undefined;
|
|
154
|
+
return {
|
|
155
|
+
username: authority.username,
|
|
156
|
+
password: authority.password,
|
|
157
|
+
host: authority.host,
|
|
158
|
+
port: authority.port,
|
|
159
|
+
path: authority.file ?? (slash >= 0 ? body.slice(slash) : ""),
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export class DatasourceUrlParser {
|
|
164
|
+
static parse(url: string | undefined): DatasourceUrl | undefined {
|
|
165
|
+
if (url === undefined || url.trim() === "") return undefined;
|
|
166
|
+
const colon = schemeColon(url);
|
|
167
|
+
if (colon === undefined) return undefined;
|
|
168
|
+
let rest = url.slice(colon + 1);
|
|
169
|
+
const hash = rest.indexOf("#");
|
|
170
|
+
if (hash >= 0) rest = rest.slice(0, hash);
|
|
171
|
+
const queryAt = rest.indexOf("?");
|
|
172
|
+
const query = queryAt >= 0 ? rest.slice(queryAt + 1) : "";
|
|
173
|
+
if (queryAt >= 0) rest = rest.slice(0, queryAt);
|
|
174
|
+
const parts = partsOf(rest);
|
|
175
|
+
const parsed = queryOf(query);
|
|
176
|
+
if (!parts || !parsed) return undefined;
|
|
177
|
+
return new DatasourceUrl(
|
|
178
|
+
url.slice(0, colon).toLowerCase(),
|
|
179
|
+
parts.username,
|
|
180
|
+
parts.password,
|
|
181
|
+
parts.host,
|
|
182
|
+
parts.port,
|
|
183
|
+
parts.path,
|
|
184
|
+
parsed.schema,
|
|
185
|
+
parsed.options,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CreateEnrichmentStrategy } from "./CreateEnrichmentStrategy.ts";
|
|
2
|
+
import { DatasourceError } from "./DatasourceError.ts";
|
|
3
|
+
import type { Enrichment } from "./Enrichment.ts";
|
|
4
|
+
import type { ICRUDRepository } from "./ICRUDRepository.ts";
|
|
5
|
+
|
|
6
|
+
export class DefaultCreateEnrichmentStrategy<T> extends CreateEnrichmentStrategy<T> {
|
|
7
|
+
async createChildren<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void> {
|
|
8
|
+
for (const related of this.payloadOf<TChild, T>(parent, enrichment.destinationProperty, enrichment)) {
|
|
9
|
+
this.stampCell(related, enrichment.relatedKey, this.getCell(parent, enrichment.sourceProperty));
|
|
10
|
+
await writer.create(related);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async createLinked<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void> {
|
|
15
|
+
const association = enrichment.association;
|
|
16
|
+
if (!association) throw new DatasourceError("Missing association");
|
|
17
|
+
const parentKey = this.requiredKey(parent, enrichment);
|
|
18
|
+
for (const id of this.relatedIds(parent, enrichment))
|
|
19
|
+
await writer.create({ [association.parent]: parentKey, [association.related]: id } as TChild);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Scalar } from "./builders/Scalar.ts";
|
|
2
|
+
import { DatasourceError } from "./DatasourceError.ts";
|
|
3
|
+
import type { Enrichment } from "./Enrichment.ts";
|
|
4
|
+
import { EnrichmentPropertyType } from "./EnrichmentPropertyType.ts";
|
|
5
|
+
import type { IReadonlyEnrichmentStrategy } from "./IReadonlyEnrichmentStrategy.ts";
|
|
6
|
+
import type { IReadonlyRepository } from "./IReadonlyRepository.ts";
|
|
7
|
+
import type { Row } from "./Row.ts";
|
|
8
|
+
import { Operator, Where } from "./statements/index.ts";
|
|
9
|
+
|
|
10
|
+
type Props = Record<string, Scalar | object>;
|
|
11
|
+
|
|
12
|
+
export class DefaultReadonlyEnrichmentStrategy<T extends object>
|
|
13
|
+
implements IReadonlyEnrichmentStrategy<T>
|
|
14
|
+
{
|
|
15
|
+
apply<TRelated extends object>(
|
|
16
|
+
via: IReadonlyRepository<TRelated>,
|
|
17
|
+
enrichment: Enrichment,
|
|
18
|
+
parents: T[],
|
|
19
|
+
sources?: (Row | undefined)[],
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const type = enrichment.type ?? EnrichmentPropertyType.Object;
|
|
22
|
+
const run = {
|
|
23
|
+
[EnrichmentPropertyType.List]: () => this.applyList(via, enrichment, parents, sources),
|
|
24
|
+
[EnrichmentPropertyType.Object]: () => this.applyObject(via, enrichment, parents, sources),
|
|
25
|
+
[EnrichmentPropertyType.Dictionary]: () => this.applyDictionary(via, enrichment, parents, sources),
|
|
26
|
+
}[type];
|
|
27
|
+
return run ? run() : Promise.reject(new DatasourceError(`Unsupported enrichment type "${type}"`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async applyLinked<TRelated extends object, TLink extends object>(
|
|
31
|
+
via: IReadonlyRepository<TRelated>,
|
|
32
|
+
link: IReadonlyRepository<TLink>,
|
|
33
|
+
enrichment: Enrichment,
|
|
34
|
+
parents: T[],
|
|
35
|
+
sources?: (Row | undefined)[],
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const association = enrichment.association ?? (() => {
|
|
38
|
+
throw new DatasourceError("Missing association");
|
|
39
|
+
})();
|
|
40
|
+
for (const [index, parent] of parents.entries()) {
|
|
41
|
+
const key = this.sourceKey(parent, enrichment.sourceProperty, sources, index);
|
|
42
|
+
if (key === undefined || key === null) continue;
|
|
43
|
+
const related: TRelated[] = [];
|
|
44
|
+
for await (const row of link.read(
|
|
45
|
+
new Where(new Operator.Equals(association.parent, this.scalar(key))),
|
|
46
|
+
)) {
|
|
47
|
+
const id = this.props(row)[association.related];
|
|
48
|
+
if (id === undefined || id === null || typeof id === "object") continue;
|
|
49
|
+
const entity = await via.readSingle(new Where(new Operator.Equals(enrichment.relatedKey, id)));
|
|
50
|
+
if (entity) related.push(entity);
|
|
51
|
+
}
|
|
52
|
+
Object.assign(parent, { [enrichment.destinationProperty]: related });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected async applyObject<TRelated extends object>(
|
|
57
|
+
via: IReadonlyRepository<TRelated>,
|
|
58
|
+
enrichment: Enrichment,
|
|
59
|
+
parents: T[],
|
|
60
|
+
sources?: (Row | undefined)[],
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const cache = new Map<string, TRelated | undefined>();
|
|
63
|
+
for (const [index, parent] of parents.entries()) {
|
|
64
|
+
const key = this.sourceKey(parent, enrichment.sourceProperty, sources, index);
|
|
65
|
+
if (key === undefined || key === null) continue;
|
|
66
|
+
const cacheKey = this.keyOf(key);
|
|
67
|
+
if (!cache.has(cacheKey))
|
|
68
|
+
cache.set(
|
|
69
|
+
cacheKey,
|
|
70
|
+
await via.readSingle(new Where(new Operator.Equals(enrichment.relatedKey, this.scalar(key)))),
|
|
71
|
+
);
|
|
72
|
+
const related = cache.get(cacheKey);
|
|
73
|
+
if (!related) continue;
|
|
74
|
+
Object.assign(parent, {
|
|
75
|
+
[enrichment.destinationProperty]: enrichment.relatedProperty ? this.props(related)[enrichment.relatedProperty] : related,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected async applyList<TRelated extends object>(
|
|
81
|
+
via: IReadonlyRepository<TRelated>,
|
|
82
|
+
enrichment: Enrichment,
|
|
83
|
+
parents: T[],
|
|
84
|
+
sources?: (Row | undefined)[],
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
for (const [index, parent] of parents.entries()) {
|
|
87
|
+
const key = this.sourceKey(parent, enrichment.sourceProperty, sources, index);
|
|
88
|
+
if (key === undefined || key === null) continue;
|
|
89
|
+
const related: TRelated[] = [];
|
|
90
|
+
for await (const item of via.read(
|
|
91
|
+
new Where(new Operator.Equals(enrichment.relatedKey, this.scalar(key))),
|
|
92
|
+
))
|
|
93
|
+
related.push(item);
|
|
94
|
+
Object.assign(parent, { [enrichment.destinationProperty]: related });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected async applyDictionary<TRelated extends object>(
|
|
99
|
+
via: IReadonlyRepository<TRelated>,
|
|
100
|
+
enrichment: Enrichment,
|
|
101
|
+
parents: T[],
|
|
102
|
+
sources?: (Row | undefined)[],
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const mapKey = mapKeyName(enrichment.relatedKey);
|
|
105
|
+
for (const [index, parent] of parents.entries()) {
|
|
106
|
+
const key = this.sourceKey(parent, enrichment.sourceProperty, sources, index);
|
|
107
|
+
if (key === undefined || key === null) continue;
|
|
108
|
+
const map: Record<string, Scalar | object> = {};
|
|
109
|
+
for await (const item of via.read(
|
|
110
|
+
new Where(new Operator.Equals(enrichment.relatedKey, this.scalar(key))),
|
|
111
|
+
)) {
|
|
112
|
+
const row = this.props(item);
|
|
113
|
+
const entry = row[mapKey];
|
|
114
|
+
if (entry === undefined || entry === null || typeof entry === "object") continue;
|
|
115
|
+
map[String(entry)] = enrichment.relatedProperty
|
|
116
|
+
? (row[enrichment.relatedProperty] ?? null)
|
|
117
|
+
: Object.fromEntries(
|
|
118
|
+
Object.entries(row).filter(
|
|
119
|
+
([name]) => name !== mapKey && name !== enrichment.relatedKey && name !== "Id" && name !== "id",
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
Object.assign(parent, { [enrichment.destinationProperty]: map });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected sourceKey(
|
|
128
|
+
parent: T,
|
|
129
|
+
name: string,
|
|
130
|
+
sources: (Row | undefined)[] | undefined,
|
|
131
|
+
index: number,
|
|
132
|
+
): Scalar | object | undefined {
|
|
133
|
+
const cell = sources?.[index]?.[name];
|
|
134
|
+
const raw = cell !== undefined && cell !== null ? cell : this.props(parent)[name];
|
|
135
|
+
return this.lookupKey(raw);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected props = <TItem>(item: TItem): Props => item as never as Props;
|
|
139
|
+
|
|
140
|
+
protected keyOf = (key: Scalar | object): string => String(this.lookupKey(key));
|
|
141
|
+
|
|
142
|
+
protected lookupKey = (value: Scalar | object | undefined): Scalar | object | undefined => {
|
|
143
|
+
if (value === undefined || value === null || typeof value !== "object") return value;
|
|
144
|
+
const id = this.props(value).Id ?? this.props(value).id;
|
|
145
|
+
return id !== undefined && id !== null && typeof id !== "object" ? id : value;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
protected scalar = (value: Scalar | object): Scalar => {
|
|
149
|
+
const key = this.lookupKey(value);
|
|
150
|
+
return key === null ||
|
|
151
|
+
typeof key === "string" ||
|
|
152
|
+
typeof key === "number" ||
|
|
153
|
+
typeof key === "boolean" ||
|
|
154
|
+
typeof key === "bigint"
|
|
155
|
+
? key
|
|
156
|
+
: (() => {
|
|
157
|
+
throw new DatasourceError("Enrichment key must be a scalar");
|
|
158
|
+
})();
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mapKeyName = (relatedKey: string): string =>
|
|
163
|
+
relatedKey.includes("_") || relatedKey[0] !== relatedKey[0]?.toUpperCase() ? "key" : "Key";
|
|
164
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DatasourceError } from "./DatasourceError.ts";
|
|
2
|
+
import { DeleteEnrichmentStrategy } from "./DeleteEnrichmentStrategy.ts";
|
|
3
|
+
import type { Enrichment } from "./Enrichment.ts";
|
|
4
|
+
import type { ICRUDRepository } from "./ICRUDRepository.ts";
|
|
5
|
+
import { UpdateEnrichmentStrategy } from "./UpdateEnrichmentStrategy.ts";
|
|
6
|
+
|
|
7
|
+
export class DefaultUpdateEnrichmentStrategy<T> extends UpdateEnrichmentStrategy<T> {
|
|
8
|
+
async updateChildren<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void> {
|
|
9
|
+
const existing = await DeleteEnrichmentStrategy.load(writer, enrichment, parent);
|
|
10
|
+
const rows = this.payloadOf<TChild, T>(parent, enrichment.destinationProperty, enrichment);
|
|
11
|
+
for (const related of rows) this.stampCell(related, enrichment.relatedKey, this.getCell(parent, enrichment.sourceProperty));
|
|
12
|
+
const keys = this.namesOf(writer, rows[0] ?? existing[0] ?? {} as TChild);
|
|
13
|
+
const keep = new Set(rows.filter((row) => this.hasKey(row, keys)).map((row) => this.keyOf(row, keys)));
|
|
14
|
+
for (const row of existing.filter((row) => !keep.has(this.keyOf(row, keys))))
|
|
15
|
+
await writer.delete(row, DeleteEnrichmentStrategy.childrenWhere(writer, enrichment, parent));
|
|
16
|
+
for (const related of rows)
|
|
17
|
+
this.hasKey(related, keys) && existing.some((row) => this.keyOf(row, keys) === this.keyOf(related, keys))
|
|
18
|
+
? await writer.update(related)
|
|
19
|
+
: await writer.create(related);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async updateLinked<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void> {
|
|
23
|
+
const association = enrichment.association;
|
|
24
|
+
if (!association) throw new DatasourceError("Missing association");
|
|
25
|
+
const parentKey = this.requiredKey(parent, enrichment);
|
|
26
|
+
const existing = await DeleteEnrichmentStrategy.loadLinks(writer, association.parent, parentKey);
|
|
27
|
+
const wanted = this.relatedIds(parent, enrichment);
|
|
28
|
+
const keep = new Set(wanted.map((id) => String(id)));
|
|
29
|
+
for (const row of existing) {
|
|
30
|
+
const id = this.getCell(row, association.related);
|
|
31
|
+
if (this.presentCell(id) && keep.has(String(id))) continue;
|
|
32
|
+
if (!this.presentCell(id)) continue;
|
|
33
|
+
await writer.delete(row, this.linkWhere(association, parentKey, id));
|
|
34
|
+
}
|
|
35
|
+
for (const id of wanted) {
|
|
36
|
+
if (existing.some((row) => String(this.getCell(row, association.related) ?? "") === String(id))) continue;
|
|
37
|
+
await writer.create({ [association.parent]: parentKey, [association.related]: id } as TChild);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|