packages/sedentary/test/testDb.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { promises } from "fs";
import { EntryBase } from "..";
import { Attribute, base, DB, loaded, size, Table, Transaction } from "../db";
const { readFile, writeFile } = promises;
function stringify(value: unknown) {
return JSON.stringify(value, (key: string, value: unknown) => (typeof value === "bigint" ? value + "n" : value));
}
class TestTransaction extends Transaction {
async commit() {
this.preCommit();
super.commit();
}
}
export class TestDB extends DB<TestTransaction> {
private body: any;
private file: string;
constructor(filename: string, log: (message: string) => void) {
super(log);
this.file = filename;
}
async begin() {
return new TestTransaction(this.log);
}
cancel(tableName: string) {
const results: Record<string, Record<string, number>> = { test1: { "": 3, "b = '1'": 1 } };
return async (where: string) => {
this.log(`Cancel from ${tableName} where: "${where}"`);
return results[tableName][where];
};
}
async connect(): Promise<void> {
this.body = { next: {}, tables: {} };
try {
this.body = JSON.parse((await readFile(this.file)).toString());
} catch(e) {
const err = e as NodeJS.ErrnoException;
if(err.code !== "ENOENT") throw e;
}
}
async end(): Promise<void> {}
async write(): Promise<void> {
await writeFile(this.file, stringify(this.body));
}
escape(value: unknown): string {
return typeof value === "number" ? value.toString() : `'${value}'`;
}
load(
tableName: string,
attributes: Record<string, string>,
pk: Attribute<unknown, unknown>,
model: new (from: "load") => EntryBase
): (where: string, order?: string | string[]) => Promise<EntryBase[]> {
const longWhere = "(fixed) AND NOT (a = 23 AND b IS NULL AND NOT c AND d > 23 AND e IN (23, 42)) OR (fixed)";
const results: Record<string, Record<string, Record<string, unknown>[]>> = {
test1: {
"": [
{ a: null, b: "test", id: 2 },
{ a: 23, b: "ok", id: 1 }
],
"a <= 10": [{ a: 2, b: "2", id: 2 }],
"a >= 23": [{ a: 23, b: { a: [1], v: "test" }, id: 1 }],
"a IS NULL": [{ a: null, b: "test", id: 2 }],
"b = 'ok'": [{ a: 23, b: "ok", id: 1 }],
"b IN ('a', 'b', 'test')": [
{ a: 23, b: "test", id: 1 },
{ a: null, b: "test", id: 2 }
],
"d = '23'": [{ a: 23, b: "ok", c: new Date("1976-01-23"), d: 23n, e: 2.3, f: true, g: { a: "b" }, id: 1 }],
"id < 23": [
{ a: 23, b: "ok", id: 1 },
{ a: null, b: "test", id: 2 }
],
"id <> 23": [{ a: 23, b: "ok", id: 1 }],
[longWhere]: [{ a: 23, b: "ok", id: 1 }]
},
test2: {
"": [
{ a: 1, b: "1", id: 1 },
{ a: 2, b: "2", id: 2 }
],
"id > 0": [
{ a: 11, b: "11", id: 1 },
{ a: 3, b: "3", id: 3 }
]
},
test3: {
"": [
{ a: 1, b: "1", id: 1 },
{ a: 2, b: "2", id: 2 }
]
}
};
return async (where: string, order?: string | string[], limit?: number, tx?: Transaction) => {
this.log(`Load from ${tableName} where: "${where}"${order ? ` order by: ${(typeof order === "string" ? [order] : order).join(", ")}` : ""}`);
return results[tableName][where].map(_ => {
const ret = new model("load");
Object.assign(ret, _);
Object.defineProperty(ret, loaded, { configurable: true, value: true });
if(tx) tx.addEntry(ret);
ret.postLoad();
return ret;
});
};
}
remove(tableName: string): (this: EntryBase & Record<string, unknown>) => Promise<number> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
let value = 1;
return async function() {
const ret = value;
self.log(`Delete from ${tableName} ${this.id}`);
value = 0;
return ret;
};
}
save(tableName: string): (this: Record<string, unknown>) => Promise<number | false> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const saves: [number | false, Record<string, unknown>][] = [
[1, { a: 23, b: "ok", id: 1 }],
[1, { a: null, b: "test", id: 2 }],
[1, { a: 23, b: "test", id: 1 }],
[false, { a: 23, b: "test", id: 1 }]
];
const saves2: Record<string, [number | false, Record<string, unknown>]> = {
'{"a":1,"b":"1"}': [1, { a: 1, b: "1" }],
'{"a":2,"b":"2"}': [1, { a: 2, b: "2" }],
'{"a":23,"b":{"a":[1,2],"v":"test"},"id":1}': [1, { a: 23, b: { a: [1, 2], v: "test" }, id: 1 }],
'{"a":3,"b":"3"}': [1, { a: 3, b: "3", id: 3 }]
};
const getSaves2 = (obj: unknown) => {
try {
//console.log(JSON.stringify(obj), saves2[JSON.stringify(obj)]);
return saves2[JSON.stringify(obj)];
} catch(e) {
return false;
}
};
return async function() {
self.log(`Save to ${tableName} ${stringify(this)}`);
const [changed, obj] = getSaves2(this) || saves.shift() || [false, {}];
Object.assign(this, obj);
return changed;
};
}
async dropConstraints(table: Table): Promise<number[]> {
const { constraints } = this.body.tables[table.tableName] || { constraints: { f: {}, u: {} } };
for(const constraint of Object.keys(constraints.f).sort()) {
const arr = table.constraints.filter(({ constraintName, type }) => constraintName === constraint && type === "f") as { attribute: { foreignKey: { options: any } } }[];
const dbOptions = arr.length ? arr[0].attribute.foreignKey.options : { onDelete: "delete", onUpdate: "delete" };
const inOptions = constraints.f[constraint].options;
if(dbOptions.onDelete !== inOptions.onDelete || dbOptions.onUpdate !== inOptions.onUpdate) {
this.syncLog(`'${table.tableName}': Removing foreign key: '${constraint}'`);
if(this.sync) delete constraints.f[constraint];
}
}
for(const constraint of Object.keys(constraints.u).sort()) {
if(! table.constraints.some(({ constraintName, type }) => constraintName === constraint && type === "u")) {
this.syncLog(`'${table.tableName}': Removing unique constraint from field: '${constraints.u[constraint].on}'`);
if(this.sync) delete constraints.u[constraint];
}
}
await this.write();
return [];
}
async dropFields(table: Table): Promise<void> {
const { fields } = this.body.tables[table.tableName] || { fields: {} };
for(const attribute of Object.keys(fields).sort()) {
const field = table.findField(attribute);
if(! field || ! field[base]) {
this.syncLog(`'${table.tableName}': Removing field: '${attribute}'`);
if(this.sync) delete fields[attribute];
}
}
await this.write();
}
async dropIndexes(table: Table): Promise<void> {
const { indexes } = this.body.tables[table.tableName] || { indexes: {} };
for(const name of Object.keys(indexes).sort()) {
const index = table.indexes.filter(_ => _.indexName === name);
if(index.length === 0 || ! this.indexesEq(indexes[name], index[0])) {
this.syncLog(`'${table.tableName}': Removing index: '${name}'`);
if(this.sync) delete indexes[name];
}
}
await this.write();
}
async syncConstraints(table: Table): Promise<void> {
const { constraints } = this.body.tables[table.tableName] || { constraints: { f: {}, u: {} } };
for(const constraint of table.constraints) {
const { attribute, constraintName, type } = constraint;
if(! constraints[type][constraintName]) {
if(type === "f") {
const { fieldName, options, tableName } = attribute.foreignKey as { attributeName: string; fieldName: string; options: any; tableName: string };
const onDelete = options.onDelete !== "no action" ? ` on delete ${options.onDelete}` : "";
const onUpdate = options.onUpdate !== "no action" ? ` on update ${options.onUpdate}` : "";
this.syncLog(`'${table.tableName}': Adding foreign key '${constraint.constraintName}' on field: '${attribute.fieldName}' references '${tableName}(${fieldName})'${onDelete}${onUpdate}`);
if(this.sync) constraints[type][constraintName] = { fieldName, on: attribute.fieldName, options, tableName };
} else {
this.syncLog(`'${table.tableName}': Adding unique constraint on field: '${attribute.fieldName}'`);
if(this.sync) constraints[type][constraintName] = { on: attribute.fieldName };
}
}
}
await this.write();
}
async syncIndexes(table: Table): Promise<void> {
const { indexes } = this.body.tables[table.tableName] || { indexes: {} };
for(const index of table.indexes) {
const { indexName } = index;
if(! (indexName in indexes)) {
this.syncLog(`'${table.tableName}': Adding index: '${indexName}' on (${index.fields.map(_ => `'${_}'`).join(", ")}) type '${index.type}'${index.unique ? " unique" : ""}`);
if(this.sync) indexes[indexName] = index;
}
}
await this.write();
}
async syncFields(table: Table): Promise<void> {
for(const attribute of table.attributes) {
const { fields } = this.body.tables[table.tableName] || { fields: {} };
const { defaultValue, fieldName, notNull, [size]: _size, type } = attribute;
let field = fields[fieldName];
if(! field && type === "NONE") continue;
if(! field) {
this.syncLog(`'${table.tableName}': Adding field: '${fieldName}' '${type}' '${_size || ""}'`);
if(this.sync) field = fields[fieldName] = { _size, type };
else field = {};
}
if(field._size !== _size || field.type !== type) {
this.syncLog(`'${table.tableName}': Changing field type: '${fieldName}' '${type}' '${_size || ""}'`);
if(this.sync) field = fields[fieldName] = { ...field, _size, type };
}
if(field.default) {
if(! defaultValue) {
this.syncLog(`'${table.tableName}': Dropping default value for field: '${fieldName}'`);
if(this.sync) delete field.default;
} else if(field.default !== (defaultValue instanceof Date ? defaultValue.toISOString() : defaultValue)) {
this.syncLog(`'${table.tableName}': Changing default value to '${defaultValue}' for field: '${fieldName}'`);
if(this.sync) field.default = defaultValue;
}
} else if(defaultValue) {
this.syncLog(`'${table.tableName}': Setting default value '${defaultValue instanceof Date ? defaultValue.toISOString() : defaultValue}' for field: '${fieldName}'`);
if(this.sync) field.default = defaultValue;
}
if(field.notNull) {
if(! notNull) {
this.syncLog(`'${table.tableName}': Dropping not null for field: '${fieldName}'`);
if(this.sync) delete field.notNull;
}
} else if(notNull) {
this.syncLog(`'${table.tableName}': Setting not null for field: '${fieldName}'`);
if(this.sync) field.notNull = true;
}
}
await this.write();
}
async syncSequence(): Promise<void> {}
async syncTable(table: Table): Promise<void> {
if(this.body.tables[table.tableName]) {
(() => {
if(table.parent) {
if(this.body.tables[table.tableName].parent === table.parent.tableName) return;
} else if(! this.body.tables[table.tableName].parent) return;
this.syncLog(`Removing table: '${table.tableName}'`);
if(this.sync) delete this.body.tables[table.tableName];
})();
}
if(! this.body.tables[table.tableName]) {
this.syncLog(`Adding table: '${table.tableName}'`);
if(this.sync) this.body.tables[table.tableName] = { constraints: { f: {}, u: {} }, fields: {}, indexes: {}, records: [] };
if(table.parent) {
this.syncLog(`Setting parent: '${table.parent.tableName}' - to table: '${table.tableName}'`);
if(this.sync) this.body.tables[table.tableName].parent = table.parent.tableName;
}
if(table.autoIncrement && ! this.body.next[table.tableName]) {
this.syncLog(`Setting auto increment: '${table.tableName}'`);
if(this.sync) this.body.tables[table.tableName].autoIncrement = 1;
}
}
await this.write();
}
}