@deterministic-code/datasources-memory 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,8 @@
1
+ import { type Cell, type DatasourceOptions, type ICommand, type IConnection, type IDriver, type ITransaction, type IsolationLevel, type TypeConverter } from "@deterministic-code/datasources";
2
+ export declare class MemoryDriver implements IDriver {
3
+ rowTypeConverters: Record<string, TypeConverter<Cell, Cell>>;
4
+ columnType: (name: string) => string;
5
+ connection: (options?: DatasourceOptions) => IConnection;
6
+ command: (connection: IConnection) => ICommand;
7
+ transaction: (connection: IConnection, isolation?: IsolationLevel) => ITransaction;
8
+ }
@@ -0,0 +1,10 @@
1
+ import { columnType as identityColumnType, } from "@deterministic-code/datasources";
2
+ import { MemoryConnection } from "./connection.js";
3
+ import { MemoryTransaction } from "./transaction.js";
4
+ export class MemoryDriver {
5
+ rowTypeConverters = {};
6
+ columnType = identityColumnType;
7
+ connection = (options) => MemoryConnection.from(options);
8
+ command = (connection) => connection.createCommand();
9
+ transaction = (connection, isolation = "readCommitted") => new MemoryTransaction(connection, isolation);
10
+ }
@@ -0,0 +1,17 @@
1
+ import { CommandMiddlewareHost, Native, type Cells, type ICommand, type IReader, type IStatement, Response, type Parameter } from "@deterministic-code/datasources";
2
+ import type { MemoryConnection } from "./connection.js";
3
+ export declare class MemoryCommand extends CommandMiddlewareHost implements ICommand {
4
+ readonly connection: MemoryConnection;
5
+ constructor(connection: MemoryConnection);
6
+ executeReaderNative(text: Native, parameters?: Parameter[]): Promise<IReader<Cells>>;
7
+ executeNative(text: Native, parameters?: Parameter[]): Promise<Response>;
8
+ executeReader(text: string, parameters?: Parameter[]): Promise<IReader<Cells>>;
9
+ executeReaderStatement: (statement: IStatement, parameters?: Parameter[]) => Promise<IReader<Cells>>;
10
+ executeStatement: (statement: IStatement, parameters?: Parameter[]) => Promise<Response>;
11
+ execute(text: string, parameters?: Parameter[]): Promise<Response>;
12
+ private write;
13
+ private applyChanged;
14
+ private removeMatched;
15
+ private applyUpdate;
16
+ private applyDelete;
17
+ }
@@ -0,0 +1,207 @@
1
+ import { ArrayReader, CommandMiddlewareHost, DeleteStatement, DefaultStatementTextRenderer, InsertStatement, SelectInsertedIdStatement, ExpressionEvaluator, UpdateStatement, Where, Response, } from "@deterministic-code/datasources";
2
+ import { matches, parametersToObject, parseFilter } from "./filter.js";
3
+ export class MemoryCommand extends CommandMiddlewareHost {
4
+ connection;
5
+ constructor(connection) {
6
+ super();
7
+ this.connection = connection;
8
+ }
9
+ executeReaderNative(text, parameters) {
10
+ return this.executeReader(text.text, parameters);
11
+ }
12
+ executeNative(text, parameters) {
13
+ return this.execute(text.text, parameters);
14
+ }
15
+ async executeReader(text, parameters) {
16
+ return this.run(text, parameters, async (query, params) => {
17
+ this.connection.ensureOpen();
18
+ const filter = {
19
+ ...parseFilter(query),
20
+ ...parametersToObject(params),
21
+ };
22
+ const rows = this.connection
23
+ .getCollection()
24
+ .filter((row) => matches(row, filter));
25
+ return { value: new ArrayReader(rows), rows: rows };
26
+ });
27
+ }
28
+ executeReaderStatement = (statement, parameters) => {
29
+ if (statement instanceof SelectInsertedIdStatement) {
30
+ return this.run(`${statement.table}.${statement.column}`, parameters, async () => {
31
+ this.connection.ensureOpen();
32
+ const id = this.connection.insertedId(statement.table);
33
+ const rows = id === undefined ? [] : [{ [statement.column]: id }];
34
+ return { value: new ArrayReader(rows), rows };
35
+ });
36
+ }
37
+ const clause = "where" in statement && statement.where instanceof Where ? statement.where : undefined;
38
+ if (clause) {
39
+ return this.run(DefaultStatementTextRenderer.instance.render(statement), parameters, async () => {
40
+ this.connection.ensureOpen();
41
+ const evaluator = new ExpressionEvaluator();
42
+ const rows = this.connection
43
+ .rows(tableOf(statement), false)
44
+ .filter((row) => evaluator.evaluate(clause, row) === true);
45
+ return { value: new ArrayReader(rows), rows: rows };
46
+ });
47
+ }
48
+ if ("where" in statement &&
49
+ !looksJson(renderWhere(statement.where)) &&
50
+ !looksJson(DefaultStatementTextRenderer.instance.render(statement))) {
51
+ return this.run(DefaultStatementTextRenderer.instance.render(statement), parameters, async () => {
52
+ this.connection.ensureOpen();
53
+ const rows = this.connection.rows(tableOf(statement), false);
54
+ return { value: new ArrayReader(rows), rows: rows };
55
+ });
56
+ }
57
+ return this.executeReader(filterText(statement), parameters);
58
+ };
59
+ executeStatement = (statement, parameters) => statement instanceof InsertStatement
60
+ ? this.write("insert", parameters, tableOf(statement), true)
61
+ : statement instanceof UpdateStatement
62
+ ? this.write("update", parameters, tableOf(statement), false, statement.where)
63
+ : statement instanceof DeleteStatement
64
+ ? this.write("delete", parameters, tableOf(statement), false, statement.where)
65
+ : this.execute(DefaultStatementTextRenderer.instance.render(statement), parameters);
66
+ async execute(text, parameters) {
67
+ if (!text.trim())
68
+ throw new Error("empty command");
69
+ return this.run(text, parameters, async (query, params) => {
70
+ this.connection.ensureOpen();
71
+ const op = query.trim().toLowerCase();
72
+ const obj = parametersToObject(params);
73
+ if (op === "insert") {
74
+ const row = { ...obj };
75
+ const id = insertedId(row, this.connection.getCollection());
76
+ row.id = id;
77
+ this.connection.getCollection().push(row);
78
+ this.connection.rememberInserted(this.connection.collection, id);
79
+ return { value: new Response(id, 1, params), rows: [] };
80
+ }
81
+ if (op === "update") {
82
+ const whereText = typeof obj.$where === "string" ? obj.$where : undefined;
83
+ const next = { ...obj };
84
+ delete next.$where;
85
+ const filter = whereText ? parseFilter(whereText) : next;
86
+ const updated = this.applyUpdate(this.connection.getCollection(), next, filter);
87
+ return { value: new Response(updated.result, updated.rowsAffected, params), rows: [] };
88
+ }
89
+ if (op === "delete") {
90
+ const filter = obj;
91
+ const deleted = this.applyDelete(this.connection.getCollection(), filter);
92
+ return { value: new Response(deleted.result, deleted.rowsAffected, params), rows: [] };
93
+ }
94
+ throw new Error(`Unsupported Memory command: ${query}`);
95
+ });
96
+ }
97
+ write = (op, parameters, table, create, where) => this.run(op, parameters, async (_query, params) => {
98
+ this.connection.ensureOpen();
99
+ const rows = this.connection.rows(table, create);
100
+ const obj = parametersToObject(params);
101
+ if (op === "insert") {
102
+ const row = { ...obj };
103
+ const id = insertedId(row, rows);
104
+ row.id = id;
105
+ rows.push(row);
106
+ this.connection.rememberInserted(table ?? this.connection.collection, id);
107
+ return { value: new Response(id, 1, params), rows: [] };
108
+ }
109
+ if (op === "update") {
110
+ const updated = this.applyChanged(rows, obj, where, params);
111
+ return { value: new Response(updated.result, updated.rowsAffected, params), rows: [] };
112
+ }
113
+ const deleted = this.removeMatched(rows, obj, where);
114
+ return { value: new Response(deleted.result, deleted.rowsAffected, params), rows: [] };
115
+ });
116
+ applyChanged = (rows, obj, where, parameters) => {
117
+ if (where instanceof Where) {
118
+ const evaluator = new ExpressionEvaluator();
119
+ const names = setNames(parameters, where);
120
+ let rowsAffected = 0;
121
+ for (const row of rows) {
122
+ if (evaluator.evaluate(where, row) !== true)
123
+ continue;
124
+ for (const name of names)
125
+ if (name in obj)
126
+ row[name] = obj[name];
127
+ rowsAffected += 1;
128
+ }
129
+ return { result: undefined, rowsAffected };
130
+ }
131
+ const next = { ...obj };
132
+ delete next.$where;
133
+ const filter = looksJson(renderWhere(where)) ? parseFilter(renderWhere(where)) : next;
134
+ return this.applyUpdate(rows, next, filter);
135
+ };
136
+ removeMatched = (rows, obj, where) => {
137
+ if (where instanceof Where) {
138
+ const evaluator = new ExpressionEvaluator();
139
+ const remaining = rows.filter((row) => evaluator.evaluate(where, row) !== true);
140
+ const rowsAffected = rows.length - remaining.length;
141
+ rows.length = 0;
142
+ rows.push(...remaining);
143
+ return { result: undefined, rowsAffected };
144
+ }
145
+ const filter = looksJson(renderWhere(where)) ? parseFilter(renderWhere(where)) : obj;
146
+ return this.applyDelete(rows, filter);
147
+ };
148
+ applyUpdate(collection, obj, filter) {
149
+ let rowsAffected = 0;
150
+ for (let i = 0; i < collection.length; i += 1) {
151
+ if (matches(collection[i], filter)) {
152
+ collection[i] = { ...collection[i], ...obj };
153
+ rowsAffected += 1;
154
+ }
155
+ }
156
+ return { result: obj, rowsAffected };
157
+ }
158
+ applyDelete(collection, filter) {
159
+ const remaining = collection.filter((row) => !matches(row, filter));
160
+ const rowsAffected = collection.length - remaining.length;
161
+ collection.length = 0;
162
+ collection.push(...remaining);
163
+ return { result: undefined, rowsAffected };
164
+ }
165
+ }
166
+ const insertedId = (row, rows) => typeof row.id === "number"
167
+ ? row.id
168
+ : rows.reduce((max, item) => (typeof item.id === "number" && item.id > max ? item.id : max), 0) + 1;
169
+ const looksJson = (text) => !!text?.trimStart().startsWith("{");
170
+ const renderWhere = (where) => where ? DefaultStatementTextRenderer.instance.render(where) : undefined;
171
+ const filterText = (statement) => {
172
+ const where = "where" in statement ? renderWhere(statement.where) : undefined;
173
+ if (looksJson(where))
174
+ return where;
175
+ const text = DefaultStatementTextRenderer.instance.render(statement);
176
+ return looksJson(text) || !text.trim() ? text : "";
177
+ };
178
+ const tableOf = (statement) => {
179
+ if ("table" in statement && typeof statement.table === "string" && statement.table.length > 0)
180
+ return statement.table;
181
+ return tableIn(DefaultStatementTextRenderer.instance.render(statement));
182
+ };
183
+ const tableIn = (text) => {
184
+ const sql = text.trimStart();
185
+ const lower = sql.toLowerCase();
186
+ const skip = lower.startsWith("insert into ")
187
+ ? "insert into ".length
188
+ : lower.startsWith("delete from ")
189
+ ? "delete from ".length
190
+ : lower.startsWith("update ")
191
+ ? "update ".length
192
+ : lower.indexOf(" from ") >= 0
193
+ ? lower.indexOf(" from ") + " from ".length
194
+ : -1;
195
+ if (skip < 0 || skip >= sql.length)
196
+ return undefined;
197
+ const rest = sql.slice(skip);
198
+ const end = rest.search(/[\s(]/);
199
+ const name = end < 0 ? rest : rest.slice(0, end);
200
+ return name.length > 0 ? name : undefined;
201
+ };
202
+ const setNames = (parameters, where) => {
203
+ const bound = new Set((where.parameters ?? []).map((parameter) => parameter.name));
204
+ return (parameters ?? [])
205
+ .map((parameter) => parameter.name)
206
+ .filter((name) => !bound.has(name) && !name.endsWith("__where"));
207
+ };
@@ -0,0 +1,6 @@
1
+ import type { MemoryRecord, MemoryStore } from "./store.js";
2
+ export interface MemoryConnectionOptions {
3
+ collection?: string;
4
+ store?: MemoryStore;
5
+ data?: MemoryRecord[] | Record<string, MemoryRecord[]>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { type DatasourceOptions, type ICommand, type IConnection, type IsolationLevelName, type ITransaction, type Scalar } from "@deterministic-code/datasources";
2
+ import type { MemoryConnectionOptions } from "./connection-options.js";
3
+ import { MemoryStore, type MemoryRecord } from "./store.js";
4
+ export declare class MemoryConnection implements IConnection {
5
+ readonly provider = "memory";
6
+ store: MemoryStore;
7
+ collection: string;
8
+ isOpen: boolean;
9
+ inTransaction: boolean;
10
+ private snapshot?;
11
+ private readonly inserted;
12
+ private readonly data?;
13
+ static from: (_options?: DatasourceOptions) => MemoryConnection;
14
+ constructor(options?: MemoryConnectionOptions);
15
+ createCommand: () => ICommand;
16
+ open(): Promise<void>;
17
+ close(): Promise<void>;
18
+ [Symbol.asyncDispose]: () => Promise<void>;
19
+ startTransaction(isolation?: IsolationLevelName): Promise<ITransaction>;
20
+ getCollection(): MemoryRecord[];
21
+ rememberInserted(table: string, id: Scalar): void;
22
+ insertedId(table: string): Scalar | undefined;
23
+ rows(table: string | undefined, create: boolean): MemoryRecord[];
24
+ finishCommit(): void;
25
+ finishAbort(): void;
26
+ ensureOpen(): void;
27
+ }
@@ -0,0 +1,69 @@
1
+ import { DatasourceError, IsolationLevel, } from "@deterministic-code/datasources";
2
+ import { MemoryCommand } from "./command.js";
3
+ import { MemoryStore } from "./store.js";
4
+ import { MemoryTransaction } from "./transaction.js";
5
+ export class MemoryConnection {
6
+ provider = "memory";
7
+ store;
8
+ collection;
9
+ isOpen = false;
10
+ inTransaction = false;
11
+ snapshot;
12
+ inserted = new Map();
13
+ data;
14
+ static from = (_options) => new MemoryConnection();
15
+ constructor(options = {}) {
16
+ this.store = options.store ?? new MemoryStore();
17
+ this.collection = options.collection ?? "";
18
+ this.data = options.data;
19
+ }
20
+ createCommand = () => new MemoryCommand(this);
21
+ async open() {
22
+ if (this.isOpen)
23
+ throw new DatasourceError("Connection is already open");
24
+ if (this.data !== undefined) {
25
+ this.store.seed(this.data, this.collection);
26
+ }
27
+ this.store.getCollection(this.collection);
28
+ this.isOpen = true;
29
+ }
30
+ async close() {
31
+ this.isOpen = false;
32
+ }
33
+ [Symbol.asyncDispose] = () => this.close();
34
+ async startTransaction(isolation = IsolationLevel.Serializable) {
35
+ this.ensureOpen();
36
+ this.snapshot = this.store.snapshot();
37
+ this.inTransaction = true;
38
+ return new MemoryTransaction(this, isolation);
39
+ }
40
+ getCollection() {
41
+ return this.store.getCollection(this.collection);
42
+ }
43
+ rememberInserted(table, id) {
44
+ this.inserted.set(table, id);
45
+ }
46
+ insertedId(table) {
47
+ return this.inserted.get(table);
48
+ }
49
+ rows(table, create) {
50
+ return table && (create || this.store.hasCollection(table))
51
+ ? this.store.getCollection(table)
52
+ : this.getCollection();
53
+ }
54
+ finishCommit() {
55
+ this.inTransaction = false;
56
+ this.snapshot = undefined;
57
+ }
58
+ finishAbort() {
59
+ if (this.snapshot) {
60
+ this.store.restore(this.snapshot);
61
+ }
62
+ this.inTransaction = false;
63
+ this.snapshot = undefined;
64
+ }
65
+ ensureOpen() {
66
+ if (!this.isOpen)
67
+ throw new DatasourceError("Connection is not open");
68
+ }
69
+ }
@@ -0,0 +1,8 @@
1
+ import type { MemoryRecord } from "./store.js";
2
+ export declare function parseFilter(text?: string | null): MemoryRecord;
3
+ export declare function matches(obj: MemoryRecord, filter: MemoryRecord): boolean;
4
+ export declare function asRecord(obj: unknown): MemoryRecord;
5
+ export declare function parametersToObject(parameters?: {
6
+ name: string;
7
+ value: unknown;
8
+ }[]): MemoryRecord;
package/dist/filter.js ADDED
@@ -0,0 +1,29 @@
1
+ export function parseFilter(text) {
2
+ if (text === undefined || text === null || text.trim() === "") {
3
+ return {};
4
+ }
5
+ const parsed = JSON.parse(text);
6
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
7
+ throw new Error("Memory filter must be an object");
8
+ }
9
+ return parsed;
10
+ }
11
+ export function matches(obj, filter) {
12
+ return Object.entries(filter).every(([key, value]) => obj[key] === value);
13
+ }
14
+ export function asRecord(obj) {
15
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
16
+ throw new Error("Repository objects must be plain objects");
17
+ }
18
+ return obj;
19
+ }
20
+ export function parametersToObject(parameters) {
21
+ const record = {};
22
+ if (!parameters) {
23
+ return record;
24
+ }
25
+ for (const parameter of parameters) {
26
+ record[parameter.name] = parameter.value;
27
+ }
28
+ return record;
29
+ }
@@ -0,0 +1 @@
1
+ export { MemoryDriver } from "./MemoryDriver.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { MemoryDriver } from "./MemoryDriver.js";
@@ -0,0 +1,9 @@
1
+ export type MemoryRecord = Record<string, unknown>;
2
+ export declare class MemoryStore {
3
+ readonly collections: Map<string, MemoryRecord[]>;
4
+ hasCollection(name: string): boolean;
5
+ getCollection(name: string): MemoryRecord[];
6
+ snapshot(): Map<string, MemoryRecord[]>;
7
+ restore(snapshot: Map<string, MemoryRecord[]>): void;
8
+ seed(data: MemoryRecord[] | Record<string, MemoryRecord[]>, collection: string): void;
9
+ }
package/dist/store.js ADDED
@@ -0,0 +1,39 @@
1
+ export class MemoryStore {
2
+ collections = new Map();
3
+ hasCollection(name) {
4
+ return this.collections.has(name);
5
+ }
6
+ getCollection(name) {
7
+ let rows = this.collections.get(name);
8
+ if (!rows) {
9
+ rows = [];
10
+ this.collections.set(name, rows);
11
+ }
12
+ return rows;
13
+ }
14
+ snapshot() {
15
+ return new Map([...this.collections].map(([name, rows]) => [
16
+ name,
17
+ rows.map((row) => ({ ...row })),
18
+ ]));
19
+ }
20
+ restore(snapshot) {
21
+ this.collections.clear();
22
+ for (const [name, rows] of snapshot) {
23
+ this.collections.set(name, rows.map((row) => ({ ...row })));
24
+ }
25
+ }
26
+ seed(data, collection) {
27
+ if (Array.isArray(data)) {
28
+ if (!this.hasCollection(collection)) {
29
+ this.collections.set(collection, data.map((row) => ({ ...row })));
30
+ }
31
+ return;
32
+ }
33
+ for (const [name, rows] of Object.entries(data)) {
34
+ if (!this.hasCollection(name)) {
35
+ this.collections.set(name, rows.map((row) => ({ ...row })));
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ import { type ITransaction, type IsolationLevelName } from "@deterministic-code/datasources";
2
+ import type { MemoryConnection } from "./connection.js";
3
+ export declare class MemoryTransaction implements ITransaction {
4
+ readonly connection: MemoryConnection;
5
+ readonly isolation: IsolationLevelName;
6
+ constructor(connection: MemoryConnection, isolation?: IsolationLevelName);
7
+ commit(): Promise<void>;
8
+ abort(): Promise<void>;
9
+ }
@@ -0,0 +1,15 @@
1
+ import { IsolationLevel, } from "@deterministic-code/datasources";
2
+ export class MemoryTransaction {
3
+ connection;
4
+ isolation;
5
+ constructor(connection, isolation = IsolationLevel.ReadCommitted) {
6
+ this.connection = connection;
7
+ this.isolation = isolation;
8
+ }
9
+ async commit() {
10
+ this.connection.finishCommit();
11
+ }
12
+ async abort() {
13
+ this.connection.finishAbort();
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "@deterministic-code/datasources-memory",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.js",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "@deterministic-code/datasources": "^0.1.2"
22
+ }
23
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.