@deterministic-code/datasources 0.1.12 → 0.1.15

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.
@@ -83,7 +83,7 @@ export class BatchCommandIBulkWriteStrategy {
83
83
  return { result: null, rowsAffected };
84
84
  };
85
85
  mappedInsert = (item) => {
86
- const record = this.reverse(item);
86
+ const record = this.repo.fillCreateZeros(this.reverse(item));
87
87
  const statement = this.statements().insert(this.fields(record, (col) => listed(this.options.createProperties, col, !this.ids.includes(col) || assigned(record[col] ?? null))));
88
88
  return { table: statement.table, fields: statement.fields };
89
89
  };
@@ -10,6 +10,8 @@ const isBulkStrategy = (value) => "create" in value &&
10
10
  !("statements" in value);
11
11
  const transactionOf = (options) => options !== undefined && "write" in options ? options.write?.useTransaction ?? false : false;
12
12
  const inTransaction = async (transactional, connection, write) => {
13
+ if (!connection.isOpen)
14
+ await connection.open();
13
15
  if (!transactional)
14
16
  return write();
15
17
  const work = new UnitOfWorkTransaction(connection);
@@ -14,6 +14,7 @@ export declare class CRUDRepository<T = Cells> extends ReadonlyRepository<T> imp
14
14
  readonly ids: readonly string[];
15
15
  private readonly createProperties?;
16
16
  private readonly updateProperties?;
17
+ protected createZeros?: Cells;
17
18
  private readonly crudStatements;
18
19
  private readonly tree;
19
20
  private readonly write;
@@ -36,6 +37,7 @@ export declare class CRUDRepository<T = Cells> extends ReadonlyRepository<T> imp
36
37
  private save;
37
38
  private remove;
38
39
  private byId;
40
+ fillCreateZeros: (record: Cells) => Cells;
39
41
  private reverseConvert;
40
42
  private idWhere;
41
43
  protected enrichmentInto: (name: string) => boolean;
@@ -6,6 +6,7 @@ import { enrichmentsOf, ReadonlyRepository } from "./ReadonlyRepository.js";
6
6
  import { DefaultReadonlyEnrichmentStrategy } from "./DefaultReadonlyEnrichmentStrategy.js";
7
7
  import { execute, executeReader } from "./ICommand.js";
8
8
  import { DatasourceError } from "./DatasourceError.js";
9
+ import { lookupKeys, mergeLookups } from "./CreateEnrichmentStrategy.js";
9
10
  import { DefaultWriteEnrichmentStrategy } from "./DefaultWriteEnrichmentStrategy.js";
10
11
  import { UnitOfWorkTransaction } from "./UnitOfWorkTransaction.js";
11
12
  import { RepositoryTree } from "./RepositoryTree.js";
@@ -13,10 +14,9 @@ import { RepositoryTreeNode } from "./RepositoryTreeNode.js";
13
14
  const assigned = (value) => value !== null && value !== "" && value !== 0 && value !== 0n;
14
15
  const listed = (list, col, fallback) => list === undefined ? fallback : list.includes(col);
15
16
  const writable = (key, columns, column) => columns.length === 0 || columns.includes(key) || columns.some((name) => column(name) === key);
16
- const fieldsOf = (record, include, cell, enrichmentInto, columns, column) => Object.entries(record)
17
+ const fieldsOf = (record, include, extras, cell, enrichmentInto, columns, column) => Object.entries(record)
17
18
  .filter(([key, value]) => include(key) &&
18
- writable(key, columns, column) &&
19
- !enrichmentInto(key) &&
19
+ (extras.has(key) || (writable(key, columns, column) && !enrichmentInto(key))) &&
20
20
  value !== null &&
21
21
  value !== undefined)
22
22
  .sort(([left], [right]) => left.localeCompare(right))
@@ -37,6 +37,7 @@ export class CRUDRepository extends ReadonlyRepository {
37
37
  ids;
38
38
  createProperties;
39
39
  updateProperties;
40
+ createZeros;
40
41
  crudStatements;
41
42
  tree;
42
43
  write;
@@ -56,9 +57,11 @@ export class CRUDRepository extends ReadonlyRepository {
56
57
  }
57
58
  addEnriched(enrichment, via, strategy) {
58
59
  super.addEnriched(enrichment, via, strategy);
60
+ const stored = lastEnrichment(this);
61
+ stored.via = via;
59
62
  const writer = writerOf(via);
60
63
  if (writer)
61
- lastEnrichment(this).persist = writer;
64
+ stored.persist = writer;
62
65
  return this;
63
66
  }
64
67
  addLinked(enrichment, via, link, strategy) {
@@ -73,9 +76,12 @@ export class CRUDRepository extends ReadonlyRepository {
73
76
  update = (item, byWhere) => this.inTransaction(() => this.updateCore(item, byWhere));
74
77
  delete = (item, byWhere) => this.inTransaction(() => this.deleteCore(item, byWhere));
75
78
  inTransaction = async (write) => {
79
+ const connection = this.command().connection;
80
+ if (!connection.isOpen)
81
+ await connection.open();
76
82
  if (!this.useTransaction)
77
83
  return write();
78
- const work = new UnitOfWorkTransaction(this.command().connection);
84
+ const work = new UnitOfWorkTransaction(connection);
79
85
  await work.begin();
80
86
  try {
81
87
  const result = await write();
@@ -88,6 +94,7 @@ export class CRUDRepository extends ReadonlyRepository {
88
94
  }
89
95
  };
90
96
  createCore = async (item) => {
97
+ await this.write.resolveLookups?.(this.tree, [item]);
91
98
  await this.insert(item);
92
99
  if (!this.enriched)
93
100
  return item;
@@ -98,6 +105,7 @@ export class CRUDRepository extends ReadonlyRepository {
98
105
  return id === undefined ? item : (await this.readSingleById(id)) ?? item;
99
106
  };
100
107
  updateCore = async (item, byWhere) => {
108
+ await this.write.resolveLookups?.(this.tree, [item]);
101
109
  const response = await this.save(item, byWhere);
102
110
  if (this.enriched)
103
111
  await this.write.persist(this.tree, [item], "update");
@@ -110,8 +118,9 @@ export class CRUDRepository extends ReadonlyRepository {
110
118
  };
111
119
  readSingleById = async (id) => this.readSingle(this.byId(id));
112
120
  insert = async (item) => {
113
- const record = this.reverseConvert(item);
114
- const fields = fieldsOf(record, (col) => listed(this.createProperties, col, !this.ids.includes(col) || assigned(record[col] ?? null)), this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column);
121
+ const extras = lookupKeys(item);
122
+ const record = this.fillCreateZeros(this.reverseConvert(item));
123
+ const fields = fieldsOf(record, (col) => extras.has(col) || listed(this.createProperties, col, !this.ids.includes(col) || assigned(record[col] ?? null)), extras, this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column);
115
124
  const statement = this.crudStatements.insert(fields);
116
125
  const response = await execute(this.command(), statement, statement.fields);
117
126
  if (this.ids.length === 1 && !fields.some((field) => field.name === this.ids[0]))
@@ -145,8 +154,9 @@ export class CRUDRepository extends ReadonlyRepository {
145
154
  };
146
155
  idValue = (item) => scalarOf(item[this.ids[0]]);
147
156
  save = async (item, byWhere) => {
157
+ const extras = lookupKeys(item);
148
158
  const record = this.reverseConvert(item);
149
- const fields = fieldsOf(record, (col) => listed(this.updateProperties, col, !this.ids.includes(col)), this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column);
159
+ const fields = fieldsOf(record, (col) => extras.has(col) || listed(this.updateProperties, col, !this.ids.includes(col)), extras, this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column);
150
160
  if (fields.length === 0)
151
161
  throw new DatasourceError("Update requires at least one non-Id field");
152
162
  const statement = this.crudStatements.update(fields, byWhere ?? this.idWhere(record), (name, value) => this.cell(name, value, byWhere === undefined));
@@ -159,7 +169,7 @@ export class CRUDRepository extends ReadonlyRepository {
159
169
  const parameters = listed.length
160
170
  ? listed
161
171
  : DefaultStatementTextRenderer.instance.render(statement).includes("$")
162
- ? fieldsOf(record, () => true, this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column)
172
+ ? fieldsOf(record, () => true, new Set(), this.cell, this.enrichmentInto, this.crudStatements.readColumns, this.column)
163
173
  : [];
164
174
  return execute(this.command(), statement, parameters);
165
175
  };
@@ -172,9 +182,12 @@ export class CRUDRepository extends ReadonlyRepository {
172
182
  throw new DatasourceError("Composite id requires a row keyed by each Id column");
173
183
  return this.idWhere({ [this.ids[0]]: id });
174
184
  };
185
+ fillCreateZeros = (record) => this.createZeros === undefined ? record : { ...this.createZeros, ...record };
175
186
  reverseConvert = (item) => {
176
187
  const row = this.objectConverter.reverseConvert(item);
177
- return this.rowConverter ? this.rowConverter.reverseConvert(row) : row;
188
+ const converted = this.rowConverter ? this.rowConverter.reverseConvert(row) : row;
189
+ mergeLookups(item, converted);
190
+ return converted;
178
191
  };
179
192
  idWhere = (record) => {
180
193
  const parts = this.ids.map((col) => {
@@ -1,13 +1,20 @@
1
1
  import type { Enrichment } from "./Enrichment.js";
2
2
  import type { ICRUDRepository } from "./ICRUDRepository.js";
3
3
  import type { Scalar } from "./builders/Scalar.js";
4
+ import type { Cells } from "./converters/Cell.js";
5
+ import type { StoredEnrichment } from "./StoredEnrichment.js";
6
+ export declare const mergeLookups: (item: object, record: Cells) => void;
7
+ export declare const lookupKeys: (item: object) => Set<string>;
4
8
  export declare abstract class CreateEnrichmentStrategy<T> {
5
9
  abstract createChildren<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void>;
6
10
  abstract createLinked<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): Promise<void>;
7
- collectChildren<TChild extends object>(writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): TChild[];
11
+ resolveLookups(entries: readonly StoredEnrichment<T>[], items: T[]): Promise<void>;
12
+ collectChildren<TChild extends object>(_writer: ICRUDRepository<TChild>, enrichment: Enrichment, parent: T): TChild[];
13
+ protected keyOf: (value: Scalar | object | undefined) => Scalar | object | undefined;
8
14
  protected getCell: <TItem>(item: TItem, name: string) => Scalar | object | undefined;
9
15
  protected presentCell: (value: Scalar | object | undefined) => value is Scalar;
10
16
  protected payloadOf: <TChild extends object, TParent>(parent: TParent, into: string, enrichment?: Enrichment) => TChild[];
17
+ protected stamp: <TItem extends object>(related: TItem, by: string, key: Scalar | object | undefined) => void;
11
18
  protected stampCell: <TItem extends object>(related: TItem, by: string, key: Scalar | object | undefined) => void;
12
19
  protected relatedIds: <TParent>(parent: TParent, enrichment: Enrichment) => Scalar[];
13
20
  protected requiredKey: <TParent>(parent: TParent, enrichment: Enrichment) => Scalar;
@@ -1,6 +1,38 @@
1
1
  import { DatasourceError } from "./DatasourceError.js";
2
+ import { Operator, Where } from "./statements/index.js";
3
+ const lookups = new WeakMap();
4
+ export const mergeLookups = (item, record) => {
5
+ const extras = lookups.get(item);
6
+ if (extras)
7
+ Object.assign(record, extras);
8
+ };
9
+ export const lookupKeys = (item) => new Set(Object.keys(lookups.get(item) ?? {}));
2
10
  export class CreateEnrichmentStrategy {
3
- collectChildren(writer, enrichment, parent) {
11
+ async resolveLookups(entries, items) {
12
+ for (const item of items) {
13
+ for (const entry of entries) {
14
+ const { enrichment } = entry;
15
+ if ((enrichment.type ?? "object") !== "object" || enrichment.association || !enrichment.relatedProperty)
16
+ continue;
17
+ const name = this.getCell(item, enrichment.destinationProperty);
18
+ if (!this.presentCell(name))
19
+ continue;
20
+ if (this.presentCell(this.keyOf(this.getCell(item, enrichment.sourceProperty))))
21
+ continue;
22
+ const via = entry.via ?? entry.persist;
23
+ if (!via)
24
+ continue;
25
+ const found = await via.readSingle(new Where(new Operator.Equals(enrichment.relatedProperty, name)));
26
+ if (!found)
27
+ throw new DatasourceError(`Unknown ${enrichment.destinationProperty} "${String(name)}"`);
28
+ const id = this.getCell(found, enrichment.relatedKey);
29
+ if (!this.presentCell(id))
30
+ throw new DatasourceError(`Unknown ${enrichment.destinationProperty} "${String(name)}"`);
31
+ this.stamp(item, enrichment.sourceProperty, id);
32
+ }
33
+ }
34
+ }
35
+ collectChildren(_writer, enrichment, parent) {
4
36
  const children = this.payloadOf(parent, enrichment.destinationProperty, enrichment);
5
37
  if (children.length === 0)
6
38
  return children;
@@ -9,10 +41,16 @@ export class CreateEnrichmentStrategy {
9
41
  this.stampCell(child, enrichment.relatedKey, key);
10
42
  return children;
11
43
  }
44
+ keyOf = (value) => {
45
+ if (value === undefined || value === null || typeof value !== "object")
46
+ return value;
47
+ const id = value.Id ?? value.id;
48
+ return id === undefined || typeof id === "object" ? undefined : id;
49
+ };
12
50
  getCell = (item, name) => item[name];
13
51
  presentCell = (value) => value !== undefined && value !== null && typeof value !== "object" && value !== 0 && value !== 0n && value !== "";
14
52
  payloadOf = (parent, into, enrichment) => mapPayload(this.getCell(parent, into), enrichment);
15
- stampCell = (related, by, key) => {
53
+ stamp = (related, by, key) => {
16
54
  if (key === undefined || key === null || typeof key === "object")
17
55
  return;
18
56
  const current = this.getCell(related, by);
@@ -22,8 +60,15 @@ export class CreateEnrichmentStrategy {
22
60
  target[id] = key;
23
61
  return;
24
62
  }
25
- Object.assign(related, { [by]: key });
63
+ if (by in related) {
64
+ Object.assign(related, { [by]: key });
65
+ return;
66
+ }
67
+ const extras = lookups.get(related) ?? {};
68
+ extras[by] = key;
69
+ lookups.set(related, extras);
26
70
  };
71
+ stampCell = (related, by, key) => this.stamp(related, by, key);
27
72
  relatedIds = (parent, enrichment) => {
28
73
  const ids = [];
29
74
  const seen = new Set();
@@ -11,6 +11,7 @@ export declare class DefaultWriteEnrichmentStrategy<T> implements IWriteEnrichme
11
11
  private readonly deleteStrategy;
12
12
  private readonly updateStrategy;
13
13
  constructor(deleteStrategy?: DeleteEnrichmentStrategy<T>, updateStrategy?: UpdateEnrichmentStrategy<T>, createStrategy?: CreateEnrichmentStrategy<T>, useTransaction?: boolean);
14
+ resolveLookups: (tree: IRepositoryTree, items: T[]) => Promise<void>;
14
15
  persist: (tree: IRepositoryTree, items: T[], mode: WriteMode) => Promise<void>;
15
16
  persistBulk: (tree: IRepositoryTree, items: T[], mode: WriteMode) => Promise<void>;
16
17
  private walk;
@@ -8,14 +8,15 @@ const unloaded = (parent, into) => {
8
8
  const value = parent[into];
9
9
  return value === undefined || value === null;
10
10
  };
11
+ const asRelated = (writer) => writer;
11
12
  const childWriter = (entry, into) => {
12
13
  if (entry.persist)
13
- return entry.persist;
14
+ return asRelated(entry.persist);
14
15
  throw new DatasourceError(`Deep write requires a CRUD repository for "${into}"`);
15
16
  };
16
17
  const linkWriter = (entry, into) => {
17
18
  if (entry.link)
18
- return entry.link;
19
+ return asRelated(entry.link);
19
20
  throw new DatasourceError(`Deep write requires a CRUD repository for "${into}"`);
20
21
  };
21
22
  const dispatch = (mode, runs) => (runs[mode] ??
@@ -34,6 +35,11 @@ export class DefaultWriteEnrichmentStrategy {
34
35
  this.createStrategy = createStrategy;
35
36
  this.useTransaction = useTransaction;
36
37
  }
38
+ resolveLookups = async (tree, items) => {
39
+ const entries = enrichmentsOf(tree.parent.value);
40
+ if (entries)
41
+ await this.createStrategy.resolveLookups(entries, items);
42
+ };
37
43
  persist = async (tree, items, mode) => {
38
44
  await this.walk(tree.parent.value, items, mode);
39
45
  };
@@ -1,5 +1,5 @@
1
1
  import type { IDriver } from "./IDriver.js";
2
2
  import type { ProviderCatalog } from "./ProviderCatalog.js";
3
3
  export declare class DriverLoader {
4
- static load: (catalog: ProviderCatalog) => IDriver;
4
+ static load(catalog: ProviderCatalog): IDriver;
5
5
  }
@@ -1,118 +1,15 @@
1
- import { readFileSync } from "node:fs";
2
- import { createRequire, registerHooks } from "node:module";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath, pathToFileURL } from "node:url";
1
+ import { createRequire } from "node:module";
2
+ import { join } from "node:path";
6
3
  import { ProviderNotInstalledError } from "./ProviderNotInstalledError.js";
7
- const require = createRequire(import.meta.url);
8
- const requirers = () => [
9
- createRequire(join(process.cwd(), "package.json")),
10
- ...(process.argv[1] ? [createRequire(process.argv[1])] : []),
11
- require,
12
- ];
13
- const missingModule = (error) => error instanceof Error &&
14
- "code" in error &&
15
- (error.code === "MODULE_NOT_FOUND" ||
16
- error.code === "ERR_MODULE_NOT_FOUND" ||
17
- error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED");
18
- const virtual = new Map();
19
- const virtualBySource = new Map();
20
- let typeScriptHook = false;
21
- let compiling = false;
22
- let compilingRoot = "";
23
- let nextVirtual = 0;
24
- /* v8 ignore start */
25
- const underRoot = (url) => {
26
- if (!url.startsWith("file:"))
27
- return false;
28
- const path = fileURLToPath(url);
29
- return path === compilingRoot || path.startsWith(`${compilingRoot}/`);
30
- };
31
- /* v8 ignore stop */
32
- const installTypeScriptHook = () => {
33
- if (typeScriptHook)
34
- return;
35
- const typescript = require("typescript");
36
- /* v8 ignore start */
37
- registerHooks({
38
- resolve: (specifier, context, nextResolve) => {
39
- const parentURL = context.parentURL ? virtual.get(context.parentURL) ?? context.parentURL : undefined;
40
- const resolved = nextResolve(specifier, parentURL ? { ...context, parentURL } : context);
41
- /* v8 ignore next */
42
- if (!compiling || !resolved.url.endsWith(".ts") || !underRoot(resolved.url))
43
- return resolved;
44
- const existing = virtualBySource.get(resolved.url);
45
- /* v8 ignore next */
46
- if (existing)
47
- return { url: existing, shortCircuit: true };
48
- const url = pathToFileURL(`${tmpdir()}/datasources-plugin-${nextVirtual++}.mjs`).href;
49
- virtual.set(url, resolved.url);
50
- virtualBySource.set(resolved.url, url);
51
- return { url, shortCircuit: true };
52
- },
53
- load: (url, context, nextLoad) => {
54
- const original = virtual.get(url) ?? (compiling && url.endsWith(".ts") && underRoot(url) ? url : undefined);
55
- /* v8 ignore next */
56
- if (!original)
57
- return nextLoad(url, context);
58
- const filename = fileURLToPath(original);
59
- const source = typescript.transpileModule(readFileSync(filename, "utf8"), {
60
- compilerOptions: {
61
- module: typescript.ModuleKind.ESNext,
62
- target: typescript.ScriptTarget.ES2022,
63
- },
64
- fileName: filename,
65
- }).outputText;
66
- return { format: "module", source, shortCircuit: true };
67
- },
68
- });
69
- /* v8 ignore stop */
70
- typeScriptHook = true;
71
- };
72
- const instantiate = (catalog) => {
73
- compiling = true;
74
- try {
75
- return instantiateLoaded(catalog);
76
- }
77
- finally {
78
- compiling = false;
79
- compilingRoot = "";
80
- }
81
- };
82
- const instantiateLoaded = (catalog) => {
83
- let last;
84
- for (const req of requirers()) {
4
+ export class DriverLoader {
5
+ static load(catalog) {
85
6
  try {
86
- compilingRoot = dirname(req.resolve(catalog.location));
87
- const loaded = req(catalog.location);
7
+ const loaded = createRequire(join(process.cwd(), "package.json"))(catalog.location);
88
8
  const Type = loaded[catalog.module];
89
- if (typeof Type !== "function")
90
- throw new ProviderNotInstalledError(catalog.name);
91
- return new Type();
9
+ return typeof Type === "function" ? new Type() : (() => { throw new ProviderNotInstalledError(catalog.name); })();
92
10
  }
93
11
  catch (error) {
94
- last = error;
95
- if (error instanceof ProviderNotInstalledError || !missingModule(error))
96
- throw error;
12
+ throw error instanceof ProviderNotInstalledError ? error : new ProviderNotInstalledError(catalog.name);
97
13
  }
98
14
  }
99
- throw last instanceof ProviderNotInstalledError ? last : new ProviderNotInstalledError(catalog.name);
100
- };
101
- export class DriverLoader {
102
- static load = (catalog) => {
103
- try {
104
- return instantiate(catalog);
105
- }
106
- catch (error) {
107
- if (error instanceof ProviderNotInstalledError)
108
- throw error;
109
- try {
110
- installTypeScriptHook();
111
- return instantiate(catalog);
112
- }
113
- catch {
114
- throw new ProviderNotInstalledError(catalog.name);
115
- }
116
- }
117
- };
118
15
  }
@@ -4,4 +4,5 @@ export interface IWriteEnrichmentStrategy<T> {
4
4
  readonly useTransaction: boolean;
5
5
  persist(tree: IRepositoryTree, items: T[], mode: WriteMode): Promise<void>;
6
6
  persistBulk?(tree: IRepositoryTree, items: T[], mode: WriteMode): Promise<void>;
7
+ resolveLookups?(tree: IRepositoryTree, items: T[]): Promise<void>;
7
8
  }
@@ -18,7 +18,7 @@ export type Sourced<T> = {
18
18
  item: T;
19
19
  source?: Row;
20
20
  };
21
- export declare const enrichmentsOf: (host: object) => readonly StoredEnrichment[] | undefined;
21
+ export declare const enrichmentsOf: <T>(host: object) => readonly StoredEnrichment<T>[] | undefined;
22
22
  export declare const isBuilt: <T>(value: IConnection | BuiltRepository<T>) => value is BuiltRepository<T>;
23
23
  export declare const builtOf: <T>(typeOrConnectionOrBuilt: Constructable<T> | IConnection | BuiltRepository<T>, connectionOrOptions?: IConnection | ReadonlyRepositoryMappingOptions<T>) => BuiltRepository<T> | undefined;
24
24
  export declare class ReadonlyRepository<T> implements IReadonlyRepository<T>, IEnriched<T> {
@@ -42,6 +42,7 @@ export declare class ReadonlyRepository<T> implements IReadonlyRepository<T>, IE
42
42
  readSingle(query?: ReadQuery, parameters?: Parameter[], options?: ReadonlyRepositoryMappingOptions<T>): Promise<T | undefined>;
43
43
  protected get enriched(): boolean;
44
44
  protected command: () => ICommand;
45
+ private ensureOpen;
45
46
  protected column: (property: string) => string;
46
47
  protected cell: (property: string, value: Cell, fieldsApplied: boolean, options?: ReadonlyRepositoryMappingOptions<T>) => Cell;
47
48
  private entries;
@@ -56,7 +56,7 @@ export class ReadonlyRepository {
56
56
  };
57
57
  addEnriched(enrichment, via, strategy) {
58
58
  const chosen = strategy ?? new DefaultReadonlyEnrichmentStrategy();
59
- this.entries().push(new StoredEnrichment(enrichment, (parents, sources) => chosen.apply(via, enrichment, parents, sources)));
59
+ this.entries().push(new StoredEnrichment(enrichment, (parents, sources) => chosen.apply(via, enrichment, parents, sources), undefined, undefined, via));
60
60
  return this;
61
61
  }
62
62
  async *read(query, parameters, options) {
@@ -72,6 +72,13 @@ export class ReadonlyRepository {
72
72
  return this.entries().length > 0;
73
73
  }
74
74
  command = () => this.middlewares.reduce((command, middleware) => command.addMiddleware(middleware), this.connection.createCommand());
75
+ ensureOpen = async () => {
76
+ const connection = this.connection;
77
+ if (!connection)
78
+ throw new DatasourceError("Connection required");
79
+ if (!connection.isOpen)
80
+ await connection.open();
81
+ };
75
82
  column = (property) => this.statementMappers.column(property);
76
83
  cell = (property, value, fieldsApplied, options) => {
77
84
  const objects = options?.objectConverter ?? this.objectConverter;
@@ -107,6 +114,7 @@ export class ReadonlyRepository {
107
114
  : objects.convert(row);
108
115
  async *readItems(query, parameters, mappers, rows, objects) {
109
116
  const statement = query ?? this.readonlyStatements.select();
117
+ await this.ensureOpen();
110
118
  const reader = await executeReader(this.command(), statement, parameters);
111
119
  try {
112
120
  while (await reader.next()) {
@@ -1,11 +1,13 @@
1
1
  import type { Enrichment } from "./Enrichment.js";
2
2
  import type { ICRUDRepository } from "./ICRUDRepository.js";
3
+ import type { IReadonlyRepository } from "./IReadonlyRepository.js";
3
4
  import type { Row } from "./Row.js";
4
- export type ApplyEnrichment<T extends object = object> = (parents: T[], sources?: (Row | undefined)[]) => Promise<void>;
5
- export declare class StoredEnrichment<T extends object = object> {
5
+ export type ApplyEnrichment<T> = (parents: T[], sources?: (Row | undefined)[]) => Promise<void>;
6
+ export declare class StoredEnrichment<T, TRelated = T, TLink = TRelated> {
6
7
  readonly enrichment: Enrichment;
7
8
  apply: ApplyEnrichment<T>;
8
- persist?: ICRUDRepository<object>;
9
- link?: ICRUDRepository<object>;
10
- constructor(enrichment: Enrichment, apply: ApplyEnrichment<T>, persist?: ICRUDRepository<object>, link?: ICRUDRepository<object>);
9
+ persist?: ICRUDRepository<TRelated>;
10
+ link?: ICRUDRepository<TLink>;
11
+ via?: IReadonlyRepository<TRelated>;
12
+ constructor(enrichment: Enrichment, apply: ApplyEnrichment<T>, persist?: ICRUDRepository<TRelated>, link?: ICRUDRepository<TLink>, via?: IReadonlyRepository<TRelated>);
11
13
  }
@@ -3,10 +3,12 @@ export class StoredEnrichment {
3
3
  apply;
4
4
  persist;
5
5
  link;
6
- constructor(enrichment, apply, persist, link) {
6
+ via;
7
+ constructor(enrichment, apply, persist, link, via) {
7
8
  this.enrichment = enrichment;
8
9
  this.apply = apply;
9
10
  this.persist = persist;
10
11
  this.link = link;
12
+ this.via = via;
11
13
  }
12
14
  }
@@ -131,7 +131,7 @@ export class DefaultValueConverter {
131
131
  const match = entry.reverse.exec(native);
132
132
  if (match === null)
133
133
  continue;
134
- const arg = match[1] ?? "";
134
+ const arg = match.slice(1).find((group) => group !== undefined) ?? "";
135
135
  return entry.specText === undefined ? name : entry.specText(arg);
136
136
  }
137
137
  throw new Error(`Native default '${native}' is not a valid ${specType} default`);
@@ -148,15 +148,17 @@ export class DefaultValueConverter {
148
148
  String: token(numeric, (arg) => arg, numeric.spec),
149
149
  };
150
150
  };
151
+ const quote = opts.quote ?? quoted;
152
+ const quoteReverse = opts.quoteReverse ?? quotedReverse;
151
153
  const quotedString = {
152
- String: token(spec.string.String, quoted, quotedReverse),
153
- Numeric: token(spec.string.String, quoted, quotedReverse),
154
+ String: token(spec.string.String, quote, quoteReverse),
155
+ Numeric: token(spec.string.String, quote, quoteReverse),
154
156
  };
155
157
  const dictionary = {
156
158
  string: quotedString,
157
159
  character: {
158
- String: token(spec.character.String, quoted, quotedReverse),
159
- Numeric: token(spec.character.String, quoted, quotedReverse),
160
+ String: token(spec.character.String, quote, quoteReverse),
161
+ Numeric: token(spec.character.String, quote, quoteReverse),
160
162
  },
161
163
  boolean: {
162
164
  Boolean: token(spec.boolean.Boolean, (arg) => (arg === "true" ? opts.booleanOn : opts.booleanOff), new RegExp(`^(${escapeRe(opts.booleanOn)}|${escapeRe(opts.booleanOff)})$`)),
@@ -6,4 +6,6 @@ export type DefaultValueOptions = {
6
6
  booleanOff: string;
7
7
  hex: (arg: string) => string;
8
8
  hexReverse: RegExp;
9
+ quote?: (arg: string) => string;
10
+ quoteReverse?: RegExp;
9
11
  };
@@ -6,6 +6,8 @@ export class ConditionRenderer {
6
6
  }
7
7
  render = (condition) => condition.accept(this);
8
8
  visit(where) {
9
- return where instanceof Where ? this.expressions.render(where.condition) : where.rawText;
9
+ return where instanceof Where || where.constructor.name === "Where"
10
+ ? this.expressions.render(where.condition)
11
+ : where.rawText;
10
12
  }
11
13
  }
@@ -1,6 +1,7 @@
1
1
  import type { Column } from "./Column.js";
2
2
  import type { CreateTableStatement } from "./CreateTableStatement.js";
3
3
  import type { ITableConstraint } from "./ITableConstraint.js";
4
+ import type { NotNull } from "./NotNull.js";
4
5
  export declare class CreateTableStatementTextRenderer {
5
6
  render(statement: CreateTableStatement): string;
6
7
  protected quote(name: string): string;
@@ -11,7 +12,7 @@ export declare class CreateTableStatementTextRenderer {
11
12
  protected column(column: Column): string;
12
13
  protected constraint(constraint: ITableConstraint): string;
13
14
  private parts;
14
- private notNullSql;
15
+ protected notNullSql(value?: NotNull): string;
15
16
  private foreignKey;
16
17
  private quoted;
17
18
  private actionSql;
@@ -4,69 +4,70 @@ import { Literal } from "./Literal.js";
4
4
  import { Operator } from "./Operator.js";
5
5
  import { Subquery } from "./Subquery.js";
6
6
  import { Where } from "./Where.js";
7
+ const is = (value, type, name) => value instanceof type || value.constructor.name === name;
7
8
  export class ExpressionRenderer {
8
9
  text;
9
10
  constructor(text) {
10
11
  this.text = text;
11
12
  }
12
13
  render = (expression) => {
13
- if (expression instanceof Where)
14
+ if (is(expression, Where, "Where"))
14
15
  return this.render(expression.condition);
15
- if (expression instanceof Identifier)
16
+ if (is(expression, Identifier, "Identifier"))
16
17
  return expression.name;
17
- if (expression instanceof Literal)
18
+ if (is(expression, Literal, "Literal"))
18
19
  return inline(expression.value);
19
- if (expression instanceof ArrayExpression)
20
+ if (is(expression, ArrayExpression, "ArrayExpression"))
20
21
  return expression.elements.map((e) => this.render(e)).join(", ");
21
- if (expression instanceof Operator.Not)
22
+ if (is(expression, Operator.Not, "Not"))
22
23
  return `NOT (${this.render(expression.argument)})`;
23
- if (expression instanceof Operator.IsNull)
24
+ if (is(expression, Operator.IsNull, "IsNull"))
24
25
  return `${this.render(expression.argument)} IS NULL`;
25
- if (expression instanceof Operator.IsNotNull)
26
+ if (is(expression, Operator.IsNotNull, "IsNotNull"))
26
27
  return `${this.render(expression.argument)} IS NOT NULL`;
27
- if (expression instanceof Operator.Exists)
28
+ if (is(expression, Operator.Exists, "Exists"))
28
29
  return `EXISTS ${this.render(expression.argument)}`;
29
- if (expression instanceof Subquery)
30
+ if (is(expression, Subquery, "Subquery"))
30
31
  return `(${this.text.render(expression.query)})`;
31
- if (expression instanceof Operator.In &&
32
- expression.left instanceof Identifier &&
33
- expression.right instanceof ArrayExpression)
32
+ if (is(expression, Operator.In, "In") &&
33
+ is(expression.left, Identifier, "Identifier") &&
34
+ is(expression.right, ArrayExpression, "ArrayExpression"))
34
35
  return expression.right.elements.length === 0
35
36
  ? "1 = 0"
36
37
  : `${expression.left.name} IN (${slots(expression.left.name, expression.right.elements.length)})`;
37
- if (expression instanceof Operator.In &&
38
- expression.left instanceof Identifier &&
39
- expression.right instanceof Subquery)
38
+ if (is(expression, Operator.In, "In") &&
39
+ is(expression.left, Identifier, "Identifier") &&
40
+ is(expression.right, Subquery, "Subquery"))
40
41
  return `${expression.left.name} IN ${this.render(expression.right)}`;
41
- if (expression instanceof Operator.NotIn &&
42
- expression.left instanceof Identifier &&
43
- expression.right instanceof ArrayExpression)
42
+ if (is(expression, Operator.NotIn, "NotIn") &&
43
+ is(expression.left, Identifier, "Identifier") &&
44
+ is(expression.right, ArrayExpression, "ArrayExpression"))
44
45
  return expression.right.elements.length === 0
45
46
  ? "1 = 1"
46
47
  : `${expression.left.name} NOT IN (${slots(expression.left.name, expression.right.elements.length)})`;
47
- if (expression instanceof Operator.NotIn &&
48
- expression.left instanceof Identifier &&
49
- expression.right instanceof Subquery)
48
+ if (is(expression, Operator.NotIn, "NotIn") &&
49
+ is(expression.left, Identifier, "Identifier") &&
50
+ is(expression.right, Subquery, "Subquery"))
50
51
  return `${expression.left.name} NOT IN ${this.render(expression.right)}`;
51
- if ((expression instanceof Operator.Between || expression instanceof Operator.NotBetween) &&
52
- expression.value instanceof Identifier &&
53
- expression.low instanceof Literal &&
54
- expression.high instanceof Literal) {
55
- const word = expression instanceof Operator.NotBetween ? "NOT BETWEEN" : "BETWEEN";
52
+ if ((is(expression, Operator.Between, "Between") || is(expression, Operator.NotBetween, "NotBetween")) &&
53
+ is(expression.value, Identifier, "Identifier") &&
54
+ is(expression.low, Literal, "Literal") &&
55
+ is(expression.high, Literal, "Literal")) {
56
+ const word = is(expression, Operator.NotBetween, "NotBetween") ? "NOT BETWEEN" : "BETWEEN";
56
57
  return `${expression.value.name} ${word} $${parameterName(expression.value.name)}_0 AND $${parameterName(expression.value.name)}_1`;
57
58
  }
58
- if (expression instanceof Operator.Between)
59
+ if (is(expression, Operator.Between, "Between"))
59
60
  return `${this.render(expression.value)} BETWEEN ${this.render(expression.low)} AND ${this.render(expression.high)}`;
60
- if (expression instanceof Operator.NotBetween)
61
+ if (is(expression, Operator.NotBetween, "NotBetween"))
61
62
  return `${this.render(expression.value)} NOT BETWEEN ${this.render(expression.low)} AND ${this.render(expression.high)}`;
62
- if (expression instanceof Operator.And)
63
+ if (is(expression, Operator.And, "And"))
63
64
  return `${this.wrap(expression.left, true)} AND ${this.wrap(expression.right, true)}`;
64
- if (expression instanceof Operator.Or)
65
+ if (is(expression, Operator.Or, "Or"))
65
66
  return `${this.wrap(expression.left, false)} OR ${this.wrap(expression.right, false)}`;
66
- const operands = expression instanceof Operator ? pair(expression) : undefined;
67
- if (operands && operands.left instanceof Identifier && operands.right instanceof Literal)
67
+ const operands = pair(expression);
68
+ if (operands && is(operands.left, Identifier, "Identifier") && is(operands.right, Literal, "Literal"))
68
69
  return `${operands.left.name} ${symbol(expression)} $${parameterName(operands.left.name)}`;
69
- if (operands && operands.left instanceof Literal && operands.right instanceof Literal)
70
+ if (operands && is(operands.left, Literal, "Literal") && is(operands.right, Literal, "Literal"))
70
71
  return `${inline(operands.left.value)} ${symbol(expression)} ${inline(operands.right.value)}`;
71
72
  if (operands)
72
73
  return `${this.render(operands.left)} ${symbol(expression)} ${this.render(operands.right)}`;
@@ -74,23 +75,34 @@ export class ExpressionRenderer {
74
75
  };
75
76
  wrap = (child, and) => needsParens(child, and) ? `(${this.render(child)})` : this.render(child);
76
77
  }
77
- const symbol = (op) => op instanceof Operator.Equals
78
- ? "="
79
- : op instanceof Operator.NotEquals
80
- ? "<>"
81
- : op instanceof Operator.GreaterThan
82
- ? ">"
83
- : op instanceof Operator.LessThan
84
- ? "<"
85
- : op instanceof Operator.GreaterThanOrEqual
86
- ? ">="
87
- : op instanceof Operator.LessThanOrEqual
88
- ? "<="
89
- : op instanceof Operator.Like
90
- ? "LIKE"
91
- : op instanceof Operator.NotLike
92
- ? "NOT LIKE"
93
- : "IN";
78
+ const symbols = {
79
+ Equals: "=",
80
+ NotEquals: "<>",
81
+ GreaterThan: ">",
82
+ LessThan: "<",
83
+ GreaterThanOrEqual: ">=",
84
+ LessThanOrEqual: "<=",
85
+ Like: "LIKE",
86
+ NotLike: "NOT LIKE",
87
+ };
88
+ const symbol = (op) => symbols[op.constructor.name] ??
89
+ (is(op, Operator.Equals, "Equals")
90
+ ? "="
91
+ : is(op, Operator.NotEquals, "NotEquals")
92
+ ? "<>"
93
+ : is(op, Operator.GreaterThan, "GreaterThan")
94
+ ? ">"
95
+ : is(op, Operator.LessThan, "LessThan")
96
+ ? "<"
97
+ : is(op, Operator.GreaterThanOrEqual, "GreaterThanOrEqual")
98
+ ? ">="
99
+ : is(op, Operator.LessThanOrEqual, "LessThanOrEqual")
100
+ ? "<="
101
+ : is(op, Operator.Like, "Like")
102
+ ? "LIKE"
103
+ : is(op, Operator.NotLike, "NotLike")
104
+ ? "NOT LIKE"
105
+ : "IN");
94
106
  const inline = (value) => value === null
95
107
  ? "null"
96
108
  : typeof value === "boolean"
@@ -98,24 +110,24 @@ const inline = (value) => value === null
98
110
  : typeof value === "string"
99
111
  ? `'${value.replace(/'/g, "''")}'`
100
112
  : String(value);
101
- const needsParens = (child, and) => child instanceof Where
113
+ const needsParens = (child, and) => is(child, Where, "Where")
102
114
  ? needsParens(child.condition, and)
103
- : child instanceof Operator.Not ||
104
- child instanceof Operator.In ||
105
- child instanceof Operator.NotIn ||
106
- (child instanceof Operator.And && !and) ||
107
- (child instanceof Operator.Or && and);
108
- const pair = (expression) => expression instanceof Operator.Equals ||
109
- expression instanceof Operator.NotEquals ||
110
- expression instanceof Operator.GreaterThan ||
111
- expression instanceof Operator.LessThan ||
112
- expression instanceof Operator.GreaterThanOrEqual ||
113
- expression instanceof Operator.LessThanOrEqual ||
114
- expression instanceof Operator.And ||
115
- expression instanceof Operator.Or ||
116
- expression instanceof Operator.In ||
117
- expression instanceof Operator.Like ||
118
- expression instanceof Operator.NotLike
115
+ : is(child, Operator.Not, "Not") ||
116
+ is(child, Operator.In, "In") ||
117
+ is(child, Operator.NotIn, "NotIn") ||
118
+ (is(child, Operator.And, "And") && !and) ||
119
+ (is(child, Operator.Or, "Or") && and);
120
+ const pair = (expression) => is(expression, Operator.Equals, "Equals") ||
121
+ is(expression, Operator.NotEquals, "NotEquals") ||
122
+ is(expression, Operator.GreaterThan, "GreaterThan") ||
123
+ is(expression, Operator.LessThan, "LessThan") ||
124
+ is(expression, Operator.GreaterThanOrEqual, "GreaterThanOrEqual") ||
125
+ is(expression, Operator.LessThanOrEqual, "LessThanOrEqual") ||
126
+ is(expression, Operator.And, "And") ||
127
+ is(expression, Operator.Or, "Or") ||
128
+ is(expression, Operator.In, "In") ||
129
+ is(expression, Operator.Like, "Like") ||
130
+ is(expression, Operator.NotLike, "NotLike")
119
131
  ? expression
120
132
  : undefined;
121
133
  const parameterName = (name) => parts(name)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deterministic-code/datasources",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.js",