cogsbox-shape 0.5.187 → 0.5.189
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/cogsbox-shape-db/dist/connect.d.ts +73 -0
- package/cogsbox-shape-db/dist/connect.js +146 -0
- package/cogsbox-shape-db/dist/errors.d.ts +7 -0
- package/cogsbox-shape-db/dist/errors.js +14 -0
- package/cogsbox-shape-db/dist/index.d.ts +4 -0
- package/cogsbox-shape-db/dist/index.js +3 -0
- package/cogsbox-shape-db/dist/remap.d.ts +5 -0
- package/cogsbox-shape-db/dist/remap.js +22 -0
- package/cogsbox-shape-db/dist/result-mapper.d.ts +7 -0
- package/cogsbox-shape-db/dist/result-mapper.js +64 -0
- package/cogsbox-shape-db/dist/sql-builder.d.ts +68 -0
- package/cogsbox-shape-db/dist/sql-builder.js +94 -0
- package/cogsbox-shape-db/dist/sqlite/index.d.ts +1 -0
- package/cogsbox-shape-db/dist/sqlite/index.js +1 -0
- package/cogsbox-shape-db/dist/sqlite/sqlite-driver.d.ts +2 -0
- package/cogsbox-shape-db/dist/sqlite/sqlite-driver.js +16 -0
- package/cogsbox-shape-db/dist/table-db.d.ts +51 -0
- package/cogsbox-shape-db/dist/table-db.js +316 -0
- package/cogsbox-shape-db/dist/types.d.ts +39 -0
- package/cogsbox-shape-db/dist/types.js +1 -0
- package/cogsbox-shape-db/dist/where-builder.d.ts +4 -0
- package/cogsbox-shape-db/dist/where-builder.js +71 -0
- package/package.json +20 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { TableDB } from "./table-db.js";
|
|
3
|
+
type FirstArg<T> = T extends (arg: infer A, ...args: any[]) => any ? A : never;
|
|
4
|
+
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
|
|
5
|
+
type Prettify<T> = {
|
|
6
|
+
[K in keyof T]: T[K];
|
|
7
|
+
} & {};
|
|
8
|
+
type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "primaryKeySQL" | "derive";
|
|
9
|
+
type SqlConfigOf<TField> = TField extends {
|
|
10
|
+
config: {
|
|
11
|
+
sql: infer TSql;
|
|
12
|
+
};
|
|
13
|
+
} ? TSql : TField extends {
|
|
14
|
+
__meta: {
|
|
15
|
+
_fieldType: infer TInner;
|
|
16
|
+
};
|
|
17
|
+
} ? SqlConfigOf<TInner> : never;
|
|
18
|
+
type SqlConfigBaseValue<TSql> = TSql extends {
|
|
19
|
+
type: "int" | "boolean";
|
|
20
|
+
} ? number : TSql extends {
|
|
21
|
+
type: "date" | "datetime" | "timestamp";
|
|
22
|
+
} ? Date : TSql extends {
|
|
23
|
+
type: "varchar" | "char" | "text" | "longtext";
|
|
24
|
+
} ? string : unknown;
|
|
25
|
+
type SqlOnlyValue<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
|
|
26
|
+
nullable: true;
|
|
27
|
+
} ? SqlConfigBaseValue<TSql> | null : SqlConfigBaseValue<TSql> : unknown;
|
|
28
|
+
type IsSqlOnlyField<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
|
|
29
|
+
sqlOnly?: infer TSqlOnly;
|
|
30
|
+
} ? true extends TSqlOnly ? true : false : false : false;
|
|
31
|
+
type IsOptionalSqlOnly<TField> = TField extends {
|
|
32
|
+
config: {
|
|
33
|
+
sql: {
|
|
34
|
+
nullable: true;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
} ? true : TField extends {
|
|
38
|
+
config: {
|
|
39
|
+
sql: {
|
|
40
|
+
default: any;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
} ? true : TField extends {
|
|
44
|
+
config: {
|
|
45
|
+
sql: {
|
|
46
|
+
defaultValue: any;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
} ? true : false;
|
|
50
|
+
type SqlOnlyInput<T> = T extends {
|
|
51
|
+
definition: infer TDefinition;
|
|
52
|
+
} ? Prettify<{
|
|
53
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? never : K : never]: SqlOnlyValue<TDefinition[K]>;
|
|
54
|
+
} & {
|
|
55
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? K : never : never]?: SqlOnlyValue<TDefinition[K]>;
|
|
56
|
+
}> : Record<string, never>;
|
|
57
|
+
type ConnectedTable<T> = T extends {
|
|
58
|
+
transforms: {
|
|
59
|
+
parseForDb: (...args: any[]) => any;
|
|
60
|
+
parseFromDb: (...args: any[]) => any;
|
|
61
|
+
};
|
|
62
|
+
} ? T & {
|
|
63
|
+
db: TableDB<Return<T["transforms"]["parseFromDb"]>, FirstArg<T["transforms"]["parseForDb"]>, SqlOnlyInput<T>>;
|
|
64
|
+
} : T;
|
|
65
|
+
type ConnectedBox<T extends Record<string, unknown>> = {
|
|
66
|
+
[K in keyof T]: ConnectedTable<T[K]>;
|
|
67
|
+
} & {
|
|
68
|
+
db: {
|
|
69
|
+
transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<unknown>): ConnectedBox<T>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { TableDB } from "./table-db.js";
|
|
3
|
+
function extractTableMeta(entry) {
|
|
4
|
+
const definition = entry.definition;
|
|
5
|
+
const tableName = definition?._tableName ?? "unknown";
|
|
6
|
+
const dbFields = new Map();
|
|
7
|
+
const clientToDbName = new Map();
|
|
8
|
+
const pkFields = [];
|
|
9
|
+
const clientPkFields = [];
|
|
10
|
+
const sqlOnlyFields = new Set();
|
|
11
|
+
const sqlOnlyClientFields = new Set();
|
|
12
|
+
const sqlOnlyRequiredClientFields = new Set();
|
|
13
|
+
const sqlOnlyValidators = new Map();
|
|
14
|
+
const deriveDependencies = new Map(Object.entries((entry.deriveDependencies ?? {})));
|
|
15
|
+
if (!definition) {
|
|
16
|
+
return {
|
|
17
|
+
tableName,
|
|
18
|
+
dbFields,
|
|
19
|
+
clientToDbName,
|
|
20
|
+
pkFields,
|
|
21
|
+
clientPkFields,
|
|
22
|
+
sqlOnlyFields,
|
|
23
|
+
sqlOnlyClientFields,
|
|
24
|
+
sqlOnlyRequiredClientFields,
|
|
25
|
+
sqlOnlyValidators,
|
|
26
|
+
deriveDependencies,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
for (const [key, field] of Object.entries(definition)) {
|
|
30
|
+
if (key === "_tableName" || key.startsWith("__"))
|
|
31
|
+
continue;
|
|
32
|
+
if (typeof field !== "object" || field === null)
|
|
33
|
+
continue;
|
|
34
|
+
const config = field.config;
|
|
35
|
+
if (!config)
|
|
36
|
+
continue;
|
|
37
|
+
const sqlConfig = config.sql;
|
|
38
|
+
if (!sqlConfig)
|
|
39
|
+
continue;
|
|
40
|
+
const type = sqlConfig.type;
|
|
41
|
+
if (type && ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(type))
|
|
42
|
+
continue;
|
|
43
|
+
const dbName = sqlConfig.field ?? key;
|
|
44
|
+
const transforms = config.transforms;
|
|
45
|
+
dbFields.set(key, {
|
|
46
|
+
dbName,
|
|
47
|
+
toDb: transforms?.toDb,
|
|
48
|
+
toClient: transforms?.toClient,
|
|
49
|
+
});
|
|
50
|
+
clientToDbName.set(key, dbName);
|
|
51
|
+
if (sqlConfig.pk)
|
|
52
|
+
pkFields.push(dbName);
|
|
53
|
+
if (sqlConfig.isClientPk)
|
|
54
|
+
clientPkFields.push(key);
|
|
55
|
+
if (sqlConfig.sqlOnly) {
|
|
56
|
+
sqlOnlyFields.add(dbName);
|
|
57
|
+
sqlOnlyClientFields.add(key);
|
|
58
|
+
if (!sqlConfig.nullable &&
|
|
59
|
+
!Object.prototype.hasOwnProperty.call(sqlConfig, "default") &&
|
|
60
|
+
!Object.prototype.hasOwnProperty.call(sqlConfig, "defaultValue")) {
|
|
61
|
+
sqlOnlyRequiredClientFields.add(key);
|
|
62
|
+
}
|
|
63
|
+
const zodSqlSchema = config.zodSqlSchema;
|
|
64
|
+
if (zodSqlSchema?.parse) {
|
|
65
|
+
sqlOnlyValidators.set(key, (val) => zodSqlSchema.parse(val));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
tableName,
|
|
71
|
+
dbFields,
|
|
72
|
+
clientToDbName,
|
|
73
|
+
pkFields,
|
|
74
|
+
clientPkFields,
|
|
75
|
+
sqlOnlyFields,
|
|
76
|
+
sqlOnlyClientFields,
|
|
77
|
+
sqlOnlyRequiredClientFields,
|
|
78
|
+
sqlOnlyValidators,
|
|
79
|
+
deriveDependencies,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function enhanceTable(entry, meta, db) {
|
|
83
|
+
const transforms = entry.transforms ?? {};
|
|
84
|
+
const tableDb = new TableDB(db, meta, {
|
|
85
|
+
toClient: transforms.toClient ?? ((r) => r),
|
|
86
|
+
toDb: transforms.toDb ?? ((r) => r),
|
|
87
|
+
parseForDb: transforms.parseForDb ?? ((r) => r),
|
|
88
|
+
parsePatchForDb: transforms.parsePatchForDb ?? transforms.toDb ?? ((r) => r),
|
|
89
|
+
parseFromDb: transforms.parseFromDb ?? ((r) => r),
|
|
90
|
+
});
|
|
91
|
+
return new Proxy(entry, {
|
|
92
|
+
get(target, prop, receiver) {
|
|
93
|
+
if (prop === "db")
|
|
94
|
+
return tableDb;
|
|
95
|
+
return Reflect.get(target, prop, receiver);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function connect(box, db) {
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const key of Object.keys(box)) {
|
|
102
|
+
const entry = box[key];
|
|
103
|
+
if (typeof entry !== "object" || entry === null) {
|
|
104
|
+
result[key] = entry;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if ("definition" in entry && "schemas" in entry && "transforms" in entry) {
|
|
108
|
+
const meta = extractTableMeta(entry);
|
|
109
|
+
result[key] = enhanceTable(entry, meta, db);
|
|
110
|
+
const originalCreateView = entry.createView;
|
|
111
|
+
if (originalCreateView) {
|
|
112
|
+
result[key].createView = (selection) => {
|
|
113
|
+
const view = originalCreateView(selection);
|
|
114
|
+
const viewMeta = { ...meta };
|
|
115
|
+
const viewTransforms = view.transforms ?? {};
|
|
116
|
+
const reconcile = view.reconcile;
|
|
117
|
+
const viewDb = new TableDB(db, viewMeta, {
|
|
118
|
+
toClient: viewTransforms.toClient ?? ((r) => r),
|
|
119
|
+
toDb: viewTransforms.toDb ?? ((r) => r),
|
|
120
|
+
parseForDb: viewTransforms.parseForDb ?? ((r) => r),
|
|
121
|
+
parsePatchForDb: viewTransforms.parsePatchForDb ?? viewTransforms.toDb ?? ((r) => r),
|
|
122
|
+
parseFromDb: viewTransforms.parseFromDb ?? ((r) => r),
|
|
123
|
+
}, reconcile);
|
|
124
|
+
return new Proxy(view, {
|
|
125
|
+
get(target, prop, receiver) {
|
|
126
|
+
if (prop === "db")
|
|
127
|
+
return viewDb;
|
|
128
|
+
return Reflect.get(target, prop, receiver);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
result[key] = entry;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const transaction = async (fn) => {
|
|
139
|
+
return db.transaction().execute(async (trx) => {
|
|
140
|
+
const txBox = connect(box, trx);
|
|
141
|
+
return fn(txBox);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
result.db = { transaction };
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class RecordNotFoundError extends Error {
|
|
2
|
+
constructor(table, pk) {
|
|
3
|
+
super(`Record not found in "${table}" with pk: ${JSON.stringify(pk)}`);
|
|
4
|
+
this.name = "RecordNotFoundError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ValidationError extends Error {
|
|
8
|
+
issues;
|
|
9
|
+
constructor(issues) {
|
|
10
|
+
super("Validation failed");
|
|
11
|
+
this.name = "ValidationError";
|
|
12
|
+
this.issues = issues;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class RemapStore {
|
|
2
|
+
map = new Map();
|
|
3
|
+
set(table, tempId, realId) {
|
|
4
|
+
let tableMap = this.map.get(table);
|
|
5
|
+
if (!tableMap) {
|
|
6
|
+
tableMap = new Map();
|
|
7
|
+
this.map.set(table, tableMap);
|
|
8
|
+
}
|
|
9
|
+
tableMap.set(tempId, realId);
|
|
10
|
+
}
|
|
11
|
+
toObject() {
|
|
12
|
+
const obj = {};
|
|
13
|
+
for (const [table, tableMap] of this.map) {
|
|
14
|
+
const entries = {};
|
|
15
|
+
for (const [temp, real] of tableMap) {
|
|
16
|
+
entries[String(temp)] = real;
|
|
17
|
+
}
|
|
18
|
+
obj[table] = entries;
|
|
19
|
+
}
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JoinDef, TableMeta } from "./types.js";
|
|
2
|
+
export declare function aliasRelationColumn(relationKey: string, field: string): string;
|
|
3
|
+
export declare function isRelationAlias(alias: string): false | {
|
|
4
|
+
relationKey: string;
|
|
5
|
+
field: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function flattenJoinedRows(rows: Record<string, unknown>[], basePkFields: string[], joins: JoinDef[], baseMeta: TableMeta, childMetas: Map<string, TableMeta>): Record<string, unknown>[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const REL_PREFIX = "__rel_";
|
|
2
|
+
export function aliasRelationColumn(relationKey, field) {
|
|
3
|
+
return `${REL_PREFIX}${relationKey}_${field}`;
|
|
4
|
+
}
|
|
5
|
+
export function isRelationAlias(alias) {
|
|
6
|
+
if (!alias.startsWith(REL_PREFIX))
|
|
7
|
+
return false;
|
|
8
|
+
const rest = alias.slice(REL_PREFIX.length);
|
|
9
|
+
const underscoreIdx = rest.indexOf("_");
|
|
10
|
+
if (underscoreIdx === -1)
|
|
11
|
+
return false;
|
|
12
|
+
return {
|
|
13
|
+
relationKey: rest.slice(0, underscoreIdx),
|
|
14
|
+
field: rest.slice(underscoreIdx + 1),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function flattenJoinedRows(rows, basePkFields, joins, baseMeta, childMetas) {
|
|
18
|
+
const groups = new Map();
|
|
19
|
+
const children = new Map();
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const pkKey = basePkFields.map(f => String(row[f] ?? "")).join("|");
|
|
22
|
+
if (!groups.has(pkKey)) {
|
|
23
|
+
const base = {};
|
|
24
|
+
for (const [clientKey, field] of baseMeta.dbFields) {
|
|
25
|
+
base[clientKey] = row[field.dbName];
|
|
26
|
+
}
|
|
27
|
+
if (baseMeta.clientToDbName.size === 0) {
|
|
28
|
+
for (const key of Object.keys(row)) {
|
|
29
|
+
if (!isRelationAlias(key)) {
|
|
30
|
+
base[key] = row[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
groups.set(pkKey, base);
|
|
35
|
+
children.set(pkKey, []);
|
|
36
|
+
}
|
|
37
|
+
for (const join of joins) {
|
|
38
|
+
const childRow = {};
|
|
39
|
+
let allNull = true;
|
|
40
|
+
for (const col of join.columns) {
|
|
41
|
+
const alias = aliasRelationColumn(join.relationKey, col);
|
|
42
|
+
const val = row[alias];
|
|
43
|
+
childRow[col] = val;
|
|
44
|
+
if (val !== null && val !== undefined) {
|
|
45
|
+
allNull = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!allNull) {
|
|
49
|
+
children.get(pkKey).push(childRow);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const [pkKey, base] of groups) {
|
|
55
|
+
const item = { ...base };
|
|
56
|
+
for (const join of joins) {
|
|
57
|
+
item[join.relationKey] = children.get(pkKey).filter(r => {
|
|
58
|
+
return Object.values(r).some(v => v !== null && v !== undefined);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
result.push(item);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { WhereClause, OrderByEntry, JoinDef } from "./types.js";
|
|
2
|
+
export interface SelectOpts {
|
|
3
|
+
table: string;
|
|
4
|
+
columns: string[];
|
|
5
|
+
where?: WhereClause;
|
|
6
|
+
orderBy?: OrderByEntry[];
|
|
7
|
+
limit?: number;
|
|
8
|
+
offset?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildSelect(opts: SelectOpts): {
|
|
11
|
+
sql: string;
|
|
12
|
+
bindings: unknown[];
|
|
13
|
+
};
|
|
14
|
+
export interface SelectWithJoinsOpts {
|
|
15
|
+
baseTable: string;
|
|
16
|
+
baseAlias: string;
|
|
17
|
+
baseColumns: string[];
|
|
18
|
+
joins: JoinDef[];
|
|
19
|
+
where?: WhereClause;
|
|
20
|
+
orderBy?: OrderByEntry[];
|
|
21
|
+
limit?: number;
|
|
22
|
+
offset?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildSelectWithJoins(opts: SelectWithJoinsOpts): {
|
|
25
|
+
sql: string;
|
|
26
|
+
bindings: unknown[];
|
|
27
|
+
};
|
|
28
|
+
export interface InsertOpts {
|
|
29
|
+
table: string;
|
|
30
|
+
values: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
export declare function buildInsert(opts: InsertOpts): {
|
|
33
|
+
sql: string;
|
|
34
|
+
bindings: unknown[];
|
|
35
|
+
};
|
|
36
|
+
export interface UpdateOpts {
|
|
37
|
+
table: string;
|
|
38
|
+
values: Record<string, unknown>;
|
|
39
|
+
where: WhereClause;
|
|
40
|
+
}
|
|
41
|
+
export declare function buildUpdate(opts: UpdateOpts): {
|
|
42
|
+
sql: string;
|
|
43
|
+
bindings: unknown[];
|
|
44
|
+
};
|
|
45
|
+
export interface DeleteOpts {
|
|
46
|
+
table: string;
|
|
47
|
+
where: WhereClause;
|
|
48
|
+
}
|
|
49
|
+
export declare function buildDelete(opts: DeleteOpts): {
|
|
50
|
+
sql: string;
|
|
51
|
+
bindings: unknown[];
|
|
52
|
+
};
|
|
53
|
+
export declare function buildCount(opts: {
|
|
54
|
+
table: string;
|
|
55
|
+
where?: WhereClause;
|
|
56
|
+
}): {
|
|
57
|
+
sql: string;
|
|
58
|
+
bindings: unknown[];
|
|
59
|
+
};
|
|
60
|
+
export declare function buildSelectById(opts: {
|
|
61
|
+
table: string;
|
|
62
|
+
columns: string[];
|
|
63
|
+
pkFields: string[];
|
|
64
|
+
pkValues: unknown[];
|
|
65
|
+
}): {
|
|
66
|
+
sql: string;
|
|
67
|
+
bindings: unknown[];
|
|
68
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function buildSelect(opts) {
|
|
2
|
+
const { table, columns, where, orderBy, limit, offset } = opts;
|
|
3
|
+
const sqlParts = [];
|
|
4
|
+
const allBindings = [];
|
|
5
|
+
sqlParts.push(`SELECT ${columns.map(c => `${table}.${c}`).join(", ")} FROM ${table}`);
|
|
6
|
+
if (where && where.sql) {
|
|
7
|
+
sqlParts.push(`WHERE ${where.sql}`);
|
|
8
|
+
allBindings.push(...where.bindings);
|
|
9
|
+
}
|
|
10
|
+
if (orderBy && orderBy.length > 0) {
|
|
11
|
+
const clauses = orderBy.map(([col, dir]) => `${col} ${dir}`);
|
|
12
|
+
sqlParts.push(`ORDER BY ${clauses.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
if (limit !== undefined) {
|
|
15
|
+
sqlParts.push(`LIMIT ?`);
|
|
16
|
+
allBindings.push(limit);
|
|
17
|
+
}
|
|
18
|
+
if (offset !== undefined) {
|
|
19
|
+
sqlParts.push(`OFFSET ?`);
|
|
20
|
+
allBindings.push(offset);
|
|
21
|
+
}
|
|
22
|
+
return { sql: sqlParts.join(" "), bindings: allBindings };
|
|
23
|
+
}
|
|
24
|
+
export function buildSelectWithJoins(opts) {
|
|
25
|
+
const { baseTable, baseAlias, baseColumns, joins, where, orderBy, limit, offset } = opts;
|
|
26
|
+
const sqlParts = [];
|
|
27
|
+
const allBindings = [];
|
|
28
|
+
const allCols = [
|
|
29
|
+
...baseColumns.map(c => `${baseAlias}.${c}`),
|
|
30
|
+
...joins.flatMap(j => j.columns.map(c => `${j.alias}.${c} AS ${c}`)),
|
|
31
|
+
];
|
|
32
|
+
sqlParts.push(`SELECT ${allCols.join(", ")} FROM ${baseTable} ${baseAlias}`);
|
|
33
|
+
for (const join of joins) {
|
|
34
|
+
sqlParts.push(`LEFT JOIN ${join.table} ${join.alias} ON ${join.on}`);
|
|
35
|
+
}
|
|
36
|
+
if (where && where.sql) {
|
|
37
|
+
sqlParts.push(`WHERE ${where.sql}`);
|
|
38
|
+
allBindings.push(...where.bindings);
|
|
39
|
+
}
|
|
40
|
+
if (orderBy && orderBy.length > 0) {
|
|
41
|
+
const clauses = orderBy.map(([col, dir]) => `${col} ${dir}`);
|
|
42
|
+
sqlParts.push(`ORDER BY ${clauses.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
if (limit !== undefined) {
|
|
45
|
+
sqlParts.push(`LIMIT ?`);
|
|
46
|
+
allBindings.push(limit);
|
|
47
|
+
}
|
|
48
|
+
if (offset !== undefined) {
|
|
49
|
+
sqlParts.push(`OFFSET ?`);
|
|
50
|
+
allBindings.push(offset);
|
|
51
|
+
}
|
|
52
|
+
return { sql: sqlParts.join(" "), bindings: allBindings };
|
|
53
|
+
}
|
|
54
|
+
export function buildInsert(opts) {
|
|
55
|
+
const { table, values } = opts;
|
|
56
|
+
const keys = Object.keys(values);
|
|
57
|
+
const placeholders = keys.map(() => "?");
|
|
58
|
+
const bindings = keys.map(k => values[k]);
|
|
59
|
+
return {
|
|
60
|
+
sql: `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders.join(", ")})`,
|
|
61
|
+
bindings,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function buildUpdate(opts) {
|
|
65
|
+
const { table, values, where } = opts;
|
|
66
|
+
const keys = Object.keys(values);
|
|
67
|
+
const setClauses = keys.map(k => `${k} = ?`);
|
|
68
|
+
const setBindings = keys.map(k => values[k]);
|
|
69
|
+
return {
|
|
70
|
+
sql: `UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where.sql}`,
|
|
71
|
+
bindings: [...setBindings, ...where.bindings],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function buildDelete(opts) {
|
|
75
|
+
return {
|
|
76
|
+
sql: `DELETE FROM ${opts.table} WHERE ${opts.where.sql}`,
|
|
77
|
+
bindings: opts.where.bindings,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function buildCount(opts) {
|
|
81
|
+
const { table, where } = opts;
|
|
82
|
+
const sqlParts = [`SELECT COUNT(*) as count FROM ${table}`];
|
|
83
|
+
const allBindings = [];
|
|
84
|
+
if (where && where.sql) {
|
|
85
|
+
sqlParts.push(`WHERE ${where.sql}`);
|
|
86
|
+
allBindings.push(...where.bindings);
|
|
87
|
+
}
|
|
88
|
+
return { sql: sqlParts.join(" "), bindings: allBindings };
|
|
89
|
+
}
|
|
90
|
+
export function buildSelectById(opts) {
|
|
91
|
+
const conditions = opts.pkFields.map(f => `${f} = ?`);
|
|
92
|
+
const sql = `SELECT ${opts.columns.map(c => `${opts.table}.${c}`).join(", ")} FROM ${opts.table} WHERE ${conditions.join(" AND ")}`;
|
|
93
|
+
return { sql, bindings: [...opts.pkValues] };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSqliteDb } from "./sqlite-driver.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSqliteDb } from "./sqlite-driver.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Kysely, SqliteDialect } from "kysely";
|
|
2
|
+
export async function createSqliteDb(path) {
|
|
3
|
+
try {
|
|
4
|
+
const mod = await import("better-sqlite3");
|
|
5
|
+
const Database = mod.default;
|
|
6
|
+
return new Kysely({
|
|
7
|
+
dialect: new SqliteDialect({
|
|
8
|
+
database: new Database(path),
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
throw new Error("Failed to initialize better-sqlite3. It may be missing, blocked by pnpm builds, or built for a different Node version.\n" +
|
|
14
|
+
"Try: pnpm approve-builds && pnpm rebuild better-sqlite3", { cause: err });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import type { TableMeta, FindManyOpts, WhereInput } from "./types.js";
|
|
3
|
+
type DbOnlyArg<T extends Record<string, unknown>> = keyof T extends never ? never : Partial<T>;
|
|
4
|
+
type RequiredKeys<T> = {
|
|
5
|
+
[K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
|
|
6
|
+
}[keyof T];
|
|
7
|
+
type InsertDbOnlyArgs<T extends Record<string, unknown>> = keyof T extends never ? [] : RequiredKeys<T> extends never ? [dbOnlyData?: Partial<T>] : [dbOnlyData: T];
|
|
8
|
+
export declare class TableDB<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> {
|
|
9
|
+
private db;
|
|
10
|
+
private meta;
|
|
11
|
+
private transforms;
|
|
12
|
+
private reconcile?;
|
|
13
|
+
constructor(db: Kysely<unknown>, meta: TableMeta, transforms: {
|
|
14
|
+
toClient: (row: Record<string, unknown>) => TClient;
|
|
15
|
+
toDb: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
16
|
+
parseForDb: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
17
|
+
parsePatchForDb: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
18
|
+
parseFromDb: (data: Record<string, unknown>) => TClient;
|
|
19
|
+
}, reconcile?: ((clientData: unknown) => {
|
|
20
|
+
withServer: (serverData: unknown) => unknown;
|
|
21
|
+
}) | undefined);
|
|
22
|
+
findMany(opts?: FindManyOpts<TClient>): Promise<TClient[]>;
|
|
23
|
+
findById(id: unknown): Promise<TClient | null>;
|
|
24
|
+
insert(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): {
|
|
25
|
+
ids: () => Promise<Record<string, unknown>>;
|
|
26
|
+
full: () => Promise<TClient>;
|
|
27
|
+
};
|
|
28
|
+
create(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): Promise<Record<string, unknown>>;
|
|
29
|
+
private insertIds;
|
|
30
|
+
update(id: unknown, data: Partial<TCreate>, dbOnlyData?: DbOnlyArg<TDbOnly>): {
|
|
31
|
+
ids: () => Promise<Record<string, unknown>>;
|
|
32
|
+
full: () => Promise<TClient>;
|
|
33
|
+
};
|
|
34
|
+
private updateIds;
|
|
35
|
+
private affectedDbBackedDerives;
|
|
36
|
+
private missingDeriveDependencies;
|
|
37
|
+
private fetchClientFieldsById;
|
|
38
|
+
private pickDbPatchFields;
|
|
39
|
+
private isWritableDbColumn;
|
|
40
|
+
private parseDbOnlyData;
|
|
41
|
+
reconcileIds(clientData: unknown, ids: unknown): unknown;
|
|
42
|
+
private reconcileFlatIds;
|
|
43
|
+
private mapIdsToClientFields;
|
|
44
|
+
private clientKeyForDbField;
|
|
45
|
+
private firstPkValue;
|
|
46
|
+
delete(id: unknown): Promise<{
|
|
47
|
+
deleted: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
count(where?: WhereInput<Partial<TClient>>): Promise<number>;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Kysely, sql } from "kysely";
|
|
2
|
+
import { buildWhereConditions, buildPkConditions } from "./where-builder.js";
|
|
3
|
+
import { RecordNotFoundError } from "./errors.js";
|
|
4
|
+
export class TableDB {
|
|
5
|
+
db;
|
|
6
|
+
meta;
|
|
7
|
+
transforms;
|
|
8
|
+
reconcile;
|
|
9
|
+
constructor(db, meta, transforms, reconcile) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.meta = meta;
|
|
12
|
+
this.transforms = transforms;
|
|
13
|
+
this.reconcile = reconcile;
|
|
14
|
+
}
|
|
15
|
+
async findMany(opts) {
|
|
16
|
+
const qb = this.db;
|
|
17
|
+
let query = qb.selectFrom(this.meta.tableName).selectAll();
|
|
18
|
+
if (opts?.where) {
|
|
19
|
+
const conditions = buildWhereConditions(opts.where, this.meta);
|
|
20
|
+
if (conditions.length > 0) {
|
|
21
|
+
query = query.where(sql.join(conditions, sql ` AND `));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (opts?.orderBy) {
|
|
25
|
+
for (const [col, dir] of Object.entries(opts.orderBy)) {
|
|
26
|
+
if (dir) {
|
|
27
|
+
const field = this.meta.dbFields.get(col);
|
|
28
|
+
const dbCol = field?.dbName ?? col;
|
|
29
|
+
query = query.orderBy(dbCol, dir);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const limit = opts?.limit ?? 100;
|
|
34
|
+
query = query.limit(limit);
|
|
35
|
+
if (opts?.offset !== undefined) {
|
|
36
|
+
query = query.offset(opts.offset);
|
|
37
|
+
}
|
|
38
|
+
const rows = (await query.execute());
|
|
39
|
+
return rows.map((r) => this.transforms.parseFromDb(r));
|
|
40
|
+
}
|
|
41
|
+
async findById(id) {
|
|
42
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
43
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
44
|
+
? this.meta.pkFields
|
|
45
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
46
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
47
|
+
const qb = this.db;
|
|
48
|
+
const rows = await qb
|
|
49
|
+
.selectFrom(this.meta.tableName)
|
|
50
|
+
.selectAll()
|
|
51
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
52
|
+
.limit(1)
|
|
53
|
+
.execute();
|
|
54
|
+
const row = rows[0] ?? null;
|
|
55
|
+
if (!row)
|
|
56
|
+
return null;
|
|
57
|
+
return this.transforms.parseFromDb(row);
|
|
58
|
+
}
|
|
59
|
+
insert(data, ...args) {
|
|
60
|
+
const dbOnlyData = args[0];
|
|
61
|
+
return {
|
|
62
|
+
ids: () => this.insertIds(data, dbOnlyData),
|
|
63
|
+
full: async () => {
|
|
64
|
+
const ids = await this.insertIds(data, dbOnlyData);
|
|
65
|
+
return this.reconcileIds(data, ids);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async create(data, ...args) {
|
|
70
|
+
const dbOnlyData = args[0];
|
|
71
|
+
return this.insertIds(data, dbOnlyData);
|
|
72
|
+
}
|
|
73
|
+
async insertIds(data, dbOnlyData) {
|
|
74
|
+
const dbData = this.transforms.parseForDb(data);
|
|
75
|
+
const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
|
|
76
|
+
requireRequired: true,
|
|
77
|
+
});
|
|
78
|
+
const clientPkClientKeys = this.meta.clientPkFields;
|
|
79
|
+
const pkDbNames = new Set(clientPkClientKeys.map((k) => {
|
|
80
|
+
const field = this.meta.dbFields.get(k);
|
|
81
|
+
return field?.dbName ?? k;
|
|
82
|
+
}));
|
|
83
|
+
const insertData = {};
|
|
84
|
+
for (const key of Object.keys(dbData)) {
|
|
85
|
+
if (!pkDbNames.has(key) && this.isWritableDbColumn(key)) {
|
|
86
|
+
insertData[key] = dbData[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
Object.assign(insertData, parsedDbOnlyData);
|
|
90
|
+
const qb = this.db;
|
|
91
|
+
const result = await qb
|
|
92
|
+
.insertInto(this.meta.tableName)
|
|
93
|
+
.values(insertData)
|
|
94
|
+
.execute();
|
|
95
|
+
const insertId = result[0]?.insertId;
|
|
96
|
+
if (insertId !== undefined && this.meta.pkFields.length > 0) {
|
|
97
|
+
const dbPkField = this.meta.pkFields[0];
|
|
98
|
+
return { [dbPkField]: Number(insertId) };
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
update(id, data, dbOnlyData) {
|
|
103
|
+
return {
|
|
104
|
+
ids: () => this.updateIds(id, data, dbOnlyData),
|
|
105
|
+
full: async () => {
|
|
106
|
+
const ids = await this.updateIds(id, data, dbOnlyData);
|
|
107
|
+
const idValue = this.firstPkValue(ids);
|
|
108
|
+
const row = await this.findById(idValue);
|
|
109
|
+
if (!row) {
|
|
110
|
+
throw new RecordNotFoundError(this.meta.tableName, idValue);
|
|
111
|
+
}
|
|
112
|
+
return row;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async updateIds(id, data, dbOnlyData) {
|
|
117
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
118
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
119
|
+
? this.meta.pkFields
|
|
120
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
121
|
+
const patchData = data;
|
|
122
|
+
const deriveKeys = this.affectedDbBackedDerives(patchData);
|
|
123
|
+
const missingDeps = this.missingDeriveDependencies(patchData, deriveKeys);
|
|
124
|
+
const fetchedDeps = missingDeps.length > 0
|
|
125
|
+
? await this.fetchClientFieldsById(pkValues, pkFields, missingDeps)
|
|
126
|
+
: {};
|
|
127
|
+
const parseInput = { ...fetchedDeps, ...patchData };
|
|
128
|
+
const parsedDbData = this.transforms.parsePatchForDb(parseInput);
|
|
129
|
+
const dbData = this.pickDbPatchFields(parsedDbData, [
|
|
130
|
+
...Object.keys(patchData),
|
|
131
|
+
...deriveKeys,
|
|
132
|
+
]);
|
|
133
|
+
Object.assign(dbData, this.parseDbOnlyData(dbOnlyData, { requireRequired: false }));
|
|
134
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
135
|
+
const qb = this.db;
|
|
136
|
+
const result = await qb
|
|
137
|
+
.updateTable(this.meta.tableName)
|
|
138
|
+
.set(dbData)
|
|
139
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
140
|
+
.execute();
|
|
141
|
+
const numUpdated = result[0]?.numUpdatedRows ?? 0n;
|
|
142
|
+
if (Number(numUpdated) === 0) {
|
|
143
|
+
throw new RecordNotFoundError(this.meta.tableName, id);
|
|
144
|
+
}
|
|
145
|
+
const pkResult = {};
|
|
146
|
+
for (let i = 0; i < pkFields.length; i++) {
|
|
147
|
+
pkResult[pkFields[i]] = pkValues[i];
|
|
148
|
+
}
|
|
149
|
+
return pkResult;
|
|
150
|
+
}
|
|
151
|
+
affectedDbBackedDerives(patchData) {
|
|
152
|
+
const patchKeys = new Set(Object.keys(patchData));
|
|
153
|
+
const affected = [];
|
|
154
|
+
for (const [deriveKey, deps] of this.meta.deriveDependencies.entries()) {
|
|
155
|
+
if (!this.meta.dbFields.has(deriveKey))
|
|
156
|
+
continue;
|
|
157
|
+
if (deps.some((dep) => patchKeys.has(dep))) {
|
|
158
|
+
affected.push(deriveKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return affected;
|
|
162
|
+
}
|
|
163
|
+
missingDeriveDependencies(patchData, deriveKeys) {
|
|
164
|
+
const patchKeys = new Set(Object.keys(patchData));
|
|
165
|
+
const missing = new Set();
|
|
166
|
+
for (const deriveKey of deriveKeys) {
|
|
167
|
+
const deps = this.meta.deriveDependencies.get(deriveKey) ?? [];
|
|
168
|
+
for (const dep of deps) {
|
|
169
|
+
if (!patchKeys.has(dep))
|
|
170
|
+
missing.add(dep);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Array.from(missing).filter((dep) => this.meta.dbFields.has(dep));
|
|
174
|
+
}
|
|
175
|
+
async fetchClientFieldsById(pkValues, pkFields, clientFields) {
|
|
176
|
+
const dbColumns = clientFields.map((clientKey) => {
|
|
177
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
178
|
+
return field?.dbName ?? clientKey;
|
|
179
|
+
});
|
|
180
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
181
|
+
const qb = this.db;
|
|
182
|
+
const row = (await qb
|
|
183
|
+
.selectFrom(this.meta.tableName)
|
|
184
|
+
.select(dbColumns)
|
|
185
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
186
|
+
.limit(1)
|
|
187
|
+
.executeTakeFirst());
|
|
188
|
+
if (!row) {
|
|
189
|
+
throw new RecordNotFoundError(this.meta.tableName, pkValues);
|
|
190
|
+
}
|
|
191
|
+
const result = {};
|
|
192
|
+
for (const clientKey of clientFields) {
|
|
193
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
194
|
+
const dbName = field?.dbName ?? clientKey;
|
|
195
|
+
const value = row[dbName];
|
|
196
|
+
result[clientKey] = field?.toClient ? field.toClient(value) : value;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
pickDbPatchFields(dbData, clientKeys) {
|
|
201
|
+
const picked = {};
|
|
202
|
+
for (const clientKey of clientKeys) {
|
|
203
|
+
const dbName = this.meta.clientToDbName.get(clientKey);
|
|
204
|
+
if (!dbName)
|
|
205
|
+
continue;
|
|
206
|
+
if (dbData[dbName] !== undefined) {
|
|
207
|
+
picked[dbName] = dbData[dbName];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return picked;
|
|
211
|
+
}
|
|
212
|
+
isWritableDbColumn(dbName) {
|
|
213
|
+
for (const field of this.meta.dbFields.values()) {
|
|
214
|
+
if (field.dbName === dbName)
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
parseDbOnlyData(dbOnlyData, opts = { requireRequired: false }) {
|
|
220
|
+
if (opts.requireRequired) {
|
|
221
|
+
for (const requiredKey of this.meta.sqlOnlyRequiredClientFields) {
|
|
222
|
+
if (!dbOnlyData || dbOnlyData[requiredKey] === undefined) {
|
|
223
|
+
throw new Error(`Missing required sqlOnly field "${requiredKey}" for "${this.meta.tableName}".`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!dbOnlyData)
|
|
228
|
+
return {};
|
|
229
|
+
const parsed = {};
|
|
230
|
+
for (const [clientKey, value] of Object.entries(dbOnlyData)) {
|
|
231
|
+
if (!this.meta.sqlOnlyClientFields.has(clientKey)) {
|
|
232
|
+
throw new Error(`Field "${clientKey}" is not a sqlOnly field on "${this.meta.tableName}".`);
|
|
233
|
+
}
|
|
234
|
+
const validator = this.meta.sqlOnlyValidators.get(clientKey);
|
|
235
|
+
const validValue = validator ? validator(value) : value;
|
|
236
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
237
|
+
const dbName = field?.dbName ?? clientKey;
|
|
238
|
+
parsed[dbName] = field?.toDb ? field.toDb(validValue) : validValue;
|
|
239
|
+
}
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
242
|
+
reconcileIds(clientData, ids) {
|
|
243
|
+
if (this.reconcile) {
|
|
244
|
+
return this.reconcile(clientData).withServer(ids);
|
|
245
|
+
}
|
|
246
|
+
return this.reconcileFlatIds(clientData, ids);
|
|
247
|
+
}
|
|
248
|
+
reconcileFlatIds(clientData, ids) {
|
|
249
|
+
if (Array.isArray(clientData)) {
|
|
250
|
+
if (!Array.isArray(ids))
|
|
251
|
+
return clientData;
|
|
252
|
+
return clientData.map((item, index) => this.reconcileFlatIds(item, ids[index]));
|
|
253
|
+
}
|
|
254
|
+
if (typeof clientData !== "object" ||
|
|
255
|
+
clientData === null ||
|
|
256
|
+
typeof ids !== "object" ||
|
|
257
|
+
ids === null) {
|
|
258
|
+
return clientData;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...clientData,
|
|
262
|
+
...this.mapIdsToClientFields(ids),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
mapIdsToClientFields(ids) {
|
|
266
|
+
const mapped = {};
|
|
267
|
+
for (const [idKey, value] of Object.entries(ids)) {
|
|
268
|
+
const clientKey = this.clientKeyForDbField(idKey);
|
|
269
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
270
|
+
mapped[clientKey] = field?.toClient ? field.toClient(value) : value;
|
|
271
|
+
}
|
|
272
|
+
return mapped;
|
|
273
|
+
}
|
|
274
|
+
clientKeyForDbField(dbField) {
|
|
275
|
+
for (const [clientKey, field] of this.meta.dbFields.entries()) {
|
|
276
|
+
if (field.dbName === dbField)
|
|
277
|
+
return clientKey;
|
|
278
|
+
}
|
|
279
|
+
return dbField;
|
|
280
|
+
}
|
|
281
|
+
firstPkValue(ids) {
|
|
282
|
+
const pkField = this.meta.pkFields[0];
|
|
283
|
+
if (pkField && ids[pkField] !== undefined) {
|
|
284
|
+
return ids[pkField];
|
|
285
|
+
}
|
|
286
|
+
return Object.values(ids)[0];
|
|
287
|
+
}
|
|
288
|
+
async delete(id) {
|
|
289
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
290
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
291
|
+
? this.meta.pkFields
|
|
292
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
293
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
294
|
+
const qb = this.db;
|
|
295
|
+
const result = await qb
|
|
296
|
+
.deleteFrom(this.meta.tableName)
|
|
297
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
298
|
+
.execute();
|
|
299
|
+
const numDeleted = result[0]?.numDeletedRows ?? 0n;
|
|
300
|
+
return { deleted: Number(numDeleted) > 0 };
|
|
301
|
+
}
|
|
302
|
+
async count(where) {
|
|
303
|
+
const qb = this.db;
|
|
304
|
+
let query = qb
|
|
305
|
+
.selectFrom(this.meta.tableName)
|
|
306
|
+
.select(sql `count(*)`.as("count"));
|
|
307
|
+
if (where) {
|
|
308
|
+
const conditions = buildWhereConditions(where, this.meta);
|
|
309
|
+
if (conditions.length > 0) {
|
|
310
|
+
query = query.where(sql.join(conditions, sql ` AND `));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const row = (await query.execute());
|
|
314
|
+
return Number(row[0]?.count ?? 0);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type WhereValue<T> = T | {
|
|
2
|
+
contains?: string;
|
|
3
|
+
startsWith?: string;
|
|
4
|
+
endsWith?: string;
|
|
5
|
+
gt?: T;
|
|
6
|
+
gte?: T;
|
|
7
|
+
lt?: T;
|
|
8
|
+
lte?: T;
|
|
9
|
+
in?: T[];
|
|
10
|
+
not?: T | Exclude<WhereValue<T>, T>;
|
|
11
|
+
};
|
|
12
|
+
export type WhereInput<T> = {
|
|
13
|
+
[K in keyof T]?: WhereValue<T[K]>;
|
|
14
|
+
};
|
|
15
|
+
export interface FindManyOpts<T> {
|
|
16
|
+
where?: WhereInput<Partial<T>>;
|
|
17
|
+
orderBy?: {
|
|
18
|
+
[K in keyof T]?: "asc" | "desc";
|
|
19
|
+
};
|
|
20
|
+
limit?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClientToDbField {
|
|
24
|
+
dbName: string;
|
|
25
|
+
toDb?: (val: any) => any;
|
|
26
|
+
toClient?: (val: any) => any;
|
|
27
|
+
}
|
|
28
|
+
export interface TableMeta {
|
|
29
|
+
tableName: string;
|
|
30
|
+
dbFields: Map<string, ClientToDbField>;
|
|
31
|
+
clientToDbName: Map<string, string>;
|
|
32
|
+
pkFields: string[];
|
|
33
|
+
clientPkFields: string[];
|
|
34
|
+
sqlOnlyFields: Set<string>;
|
|
35
|
+
sqlOnlyClientFields: Set<string>;
|
|
36
|
+
sqlOnlyRequiredClientFields: Set<string>;
|
|
37
|
+
sqlOnlyValidators: Map<string, (val: unknown) => unknown>;
|
|
38
|
+
deriveDependencies: Map<string, string[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TableMeta } from "./types.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
export declare function buildWhereConditions(filter: Record<string, unknown>, meta: TableMeta): ReturnType<typeof sql>[];
|
|
4
|
+
export declare function buildPkConditions(pkValues: unknown[], pkFields: string[]): ReturnType<typeof sql>[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
export function buildWhereConditions(filter, meta) {
|
|
3
|
+
const conditions = [];
|
|
4
|
+
for (const [clientKey, value] of Object.entries(filter)) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
continue;
|
|
7
|
+
const field = meta.dbFields.get(clientKey);
|
|
8
|
+
const dbName = field?.dbName ?? clientKey;
|
|
9
|
+
const dbRef = sql.ref(dbName);
|
|
10
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
11
|
+
const op = value;
|
|
12
|
+
if ("contains" in op) {
|
|
13
|
+
const tv = field?.toDb ? field.toDb(op.contains) : op.contains;
|
|
14
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
|
|
15
|
+
}
|
|
16
|
+
else if ("startsWith" in op) {
|
|
17
|
+
const tv = field?.toDb ? field.toDb(op.startsWith) : op.startsWith;
|
|
18
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`${String(tv ?? "")}%`)}`);
|
|
19
|
+
}
|
|
20
|
+
else if ("endsWith" in op) {
|
|
21
|
+
const tv = field?.toDb ? field.toDb(op.endsWith) : op.endsWith;
|
|
22
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}`)}`);
|
|
23
|
+
}
|
|
24
|
+
else if ("gt" in op) {
|
|
25
|
+
const tv = field?.toDb ? field.toDb(op.gt) : op.gt;
|
|
26
|
+
conditions.push(sql `${dbRef} > ${sql.val(tv)}`);
|
|
27
|
+
}
|
|
28
|
+
else if ("gte" in op) {
|
|
29
|
+
const tv = field?.toDb ? field.toDb(op.gte) : op.gte;
|
|
30
|
+
conditions.push(sql `${dbRef} >= ${sql.val(tv)}`);
|
|
31
|
+
}
|
|
32
|
+
else if ("lt" in op) {
|
|
33
|
+
const tv = field?.toDb ? field.toDb(op.lt) : op.lt;
|
|
34
|
+
conditions.push(sql `${dbRef} < ${sql.val(tv)}`);
|
|
35
|
+
}
|
|
36
|
+
else if ("lte" in op) {
|
|
37
|
+
const tv = field?.toDb ? field.toDb(op.lte) : op.lte;
|
|
38
|
+
conditions.push(sql `${dbRef} <= ${sql.val(tv)}`);
|
|
39
|
+
}
|
|
40
|
+
else if ("in" in op && Array.isArray(op.in)) {
|
|
41
|
+
const items = op.in.map((v) => {
|
|
42
|
+
const tv = field?.toDb ? field.toDb(v) : v;
|
|
43
|
+
return sql.val(tv);
|
|
44
|
+
});
|
|
45
|
+
conditions.push(sql `${dbRef} IN (${sql.join(items, sql `, `)})`);
|
|
46
|
+
}
|
|
47
|
+
else if ("not" in op) {
|
|
48
|
+
const nv = op.not;
|
|
49
|
+
if (nv !== null && typeof nv === "object" && !Array.isArray(nv)) {
|
|
50
|
+
const no = nv;
|
|
51
|
+
if ("contains" in no) {
|
|
52
|
+
const tv = field?.toDb ? field.toDb(no.contains) : no.contains;
|
|
53
|
+
conditions.push(sql `${dbRef} NOT LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const tv = field?.toDb ? field.toDb(nv) : nv;
|
|
58
|
+
conditions.push(sql `${dbRef} != ${sql.val(tv)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const transformed = field?.toDb ? field.toDb(value) : value;
|
|
64
|
+
conditions.push(sql `${dbRef} = ${sql.val(transformed)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return conditions;
|
|
68
|
+
}
|
|
69
|
+
export function buildPkConditions(pkValues, pkFields) {
|
|
70
|
+
return pkFields.map((f, i) => sql `${sql.ref(f)} = ${sql.val(pkValues[i])}`);
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogsbox-shape",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.189",
|
|
4
4
|
"description": "A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for relationships and serialization.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"dist"
|
|
8
|
+
"dist",
|
|
9
|
+
"cogsbox-shape-db/dist"
|
|
9
10
|
],
|
|
10
11
|
"repository": "github:cogsbox/cogsbox-shape",
|
|
11
12
|
"scripts": {
|
|
12
|
-
"build": "
|
|
13
|
+
"build": "tsc && pnpm --dir cogsbox-shape-db run build",
|
|
13
14
|
"lint": "eslint src --ext .ts",
|
|
14
15
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
15
16
|
"test": "vitest ",
|
|
@@ -19,7 +20,18 @@
|
|
|
19
20
|
"cogsbox-shape": "./dist/cli.js"
|
|
20
21
|
},
|
|
21
22
|
"exports": {
|
|
22
|
-
".":
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./db": {
|
|
28
|
+
"types": "./cogsbox-shape-db/dist/index.d.ts",
|
|
29
|
+
"import": "./cogsbox-shape-db/dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./db/sqlite": {
|
|
32
|
+
"types": "./cogsbox-shape-db/dist/sqlite/index.d.ts",
|
|
33
|
+
"import": "./cogsbox-shape-db/dist/sqlite/index.js"
|
|
34
|
+
}
|
|
23
35
|
},
|
|
24
36
|
"keywords": [
|
|
25
37
|
"typescript",
|
|
@@ -38,9 +50,13 @@
|
|
|
38
50
|
"license": "MIT",
|
|
39
51
|
"dependencies": {
|
|
40
52
|
"commander": "^13.1.0",
|
|
53
|
+
"kysely": "^0.29.2",
|
|
41
54
|
"tsx": "^4.19.3",
|
|
42
55
|
"uuid": "^11.0.0"
|
|
43
56
|
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"better-sqlite3": "^11.7.0"
|
|
59
|
+
},
|
|
44
60
|
"devDependencies": {
|
|
45
61
|
"@types/node": "^20.10.5",
|
|
46
62
|
"@types/uuid": "^11.0.0",
|