sqlite-zod-orm 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -381
- package/dist/index.js +195 -10
- package/package.json +1 -1
- package/src/builder.ts +101 -0
- package/src/context.ts +9 -0
- package/src/crud.ts +39 -4
- package/src/database.ts +79 -3
- package/src/helpers.ts +11 -0
- package/src/index.ts +1 -1
- package/src/iqo.ts +29 -3
- package/src/query.ts +7 -0
- package/src/types.ts +27 -1
package/dist/index.js
CHANGED
|
@@ -4149,7 +4149,9 @@ var OPERATOR_MAP = {
|
|
|
4149
4149
|
$in: "IN",
|
|
4150
4150
|
$like: "LIKE",
|
|
4151
4151
|
$notIn: "NOT IN",
|
|
4152
|
-
$between: "BETWEEN"
|
|
4152
|
+
$between: "BETWEEN",
|
|
4153
|
+
$isNull: "IS NULL",
|
|
4154
|
+
$isNotNull: "IS NOT NULL"
|
|
4153
4155
|
};
|
|
4154
4156
|
function transformValueForStorage(value) {
|
|
4155
4157
|
if (value instanceof Date)
|
|
@@ -4173,7 +4175,7 @@ function compileIQO(tableName, iqo) {
|
|
|
4173
4175
|
selectParts.push(`${j.table}.*`);
|
|
4174
4176
|
}
|
|
4175
4177
|
}
|
|
4176
|
-
let sql = `SELECT ${selectParts.join(", ")} FROM ${tableName}`;
|
|
4178
|
+
let sql = `SELECT ${iqo.distinct ? "DISTINCT " : ""}${selectParts.join(", ")} FROM ${tableName}`;
|
|
4177
4179
|
for (const j of iqo.joins) {
|
|
4178
4180
|
sql += ` JOIN ${j.table} ON ${tableName}.${j.fromCol} = ${j.table}.${j.toCol}`;
|
|
4179
4181
|
}
|
|
@@ -4206,6 +4208,10 @@ function compileIQO(tableName, iqo) {
|
|
|
4206
4208
|
const [min, max] = w.value;
|
|
4207
4209
|
whereParts.push(`${qualify(w.field)} BETWEEN ? AND ?`);
|
|
4208
4210
|
params.push(transformValueForStorage(min), transformValueForStorage(max));
|
|
4211
|
+
} else if (w.operator === "IS NULL") {
|
|
4212
|
+
whereParts.push(`${qualify(w.field)} IS NULL`);
|
|
4213
|
+
} else if (w.operator === "IS NOT NULL") {
|
|
4214
|
+
whereParts.push(`${qualify(w.field)} IS NOT NULL`);
|
|
4209
4215
|
} else {
|
|
4210
4216
|
whereParts.push(`${qualify(w.field)} ${w.operator} ?`);
|
|
4211
4217
|
params.push(transformValueForStorage(w.value));
|
|
@@ -4241,6 +4247,22 @@ function compileIQO(tableName, iqo) {
|
|
|
4241
4247
|
if (iqo.groupBy.length > 0) {
|
|
4242
4248
|
sql += ` GROUP BY ${iqo.groupBy.join(", ")}`;
|
|
4243
4249
|
}
|
|
4250
|
+
if (iqo.having && iqo.having.length > 0) {
|
|
4251
|
+
const havingParts = [];
|
|
4252
|
+
for (const h of iqo.having) {
|
|
4253
|
+
if (h.operator === "IS NULL") {
|
|
4254
|
+
havingParts.push(`${h.field} IS NULL`);
|
|
4255
|
+
} else if (h.operator === "IS NOT NULL") {
|
|
4256
|
+
havingParts.push(`${h.field} IS NOT NULL`);
|
|
4257
|
+
} else {
|
|
4258
|
+
havingParts.push(`${h.field} ${h.operator} ?`);
|
|
4259
|
+
params.push(transformValueForStorage(h.value));
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
if (havingParts.length > 0) {
|
|
4263
|
+
sql += ` HAVING ${havingParts.join(" AND ")}`;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4244
4266
|
if (iqo.orderBy.length > 0) {
|
|
4245
4267
|
const parts = iqo.orderBy.map((o) => `${o.field} ${o.direction.toUpperCase()}`);
|
|
4246
4268
|
sql += ` ORDER BY ${parts.join(", ")}`;
|
|
@@ -4274,11 +4296,13 @@ class QueryBuilder {
|
|
|
4274
4296
|
whereAST: null,
|
|
4275
4297
|
joins: [],
|
|
4276
4298
|
groupBy: [],
|
|
4299
|
+
having: [],
|
|
4277
4300
|
limit: null,
|
|
4278
4301
|
offset: null,
|
|
4279
4302
|
orderBy: [],
|
|
4280
4303
|
includes: [],
|
|
4281
|
-
raw: false
|
|
4304
|
+
raw: false,
|
|
4305
|
+
distinct: false
|
|
4282
4306
|
};
|
|
4283
4307
|
}
|
|
4284
4308
|
select(...cols) {
|
|
@@ -4433,6 +4457,66 @@ class QueryBuilder {
|
|
|
4433
4457
|
this.iqo.groupBy.push(...fields);
|
|
4434
4458
|
return this;
|
|
4435
4459
|
}
|
|
4460
|
+
distinct() {
|
|
4461
|
+
this.iqo.distinct = true;
|
|
4462
|
+
return this;
|
|
4463
|
+
}
|
|
4464
|
+
withTrashed() {
|
|
4465
|
+
this.iqo.wheres = this.iqo.wheres.filter((w) => !(w.field === "deletedAt" && w.operator === "IS NULL"));
|
|
4466
|
+
return this;
|
|
4467
|
+
}
|
|
4468
|
+
onlyTrashed() {
|
|
4469
|
+
this.iqo.wheres = this.iqo.wheres.filter((w) => !(w.field === "deletedAt" && w.operator === "IS NULL"));
|
|
4470
|
+
this.iqo.wheres.push({ field: "deletedAt", operator: "IS NOT NULL", value: null });
|
|
4471
|
+
return this;
|
|
4472
|
+
}
|
|
4473
|
+
having(conditions) {
|
|
4474
|
+
for (const [field, value] of Object.entries(conditions)) {
|
|
4475
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
4476
|
+
for (const [opKey, operand] of Object.entries(value)) {
|
|
4477
|
+
const sqlOp = OPERATOR_MAP[opKey];
|
|
4478
|
+
if (!sqlOp)
|
|
4479
|
+
throw new Error(`Unsupported having operator: '${opKey}'`);
|
|
4480
|
+
this.iqo.having.push({ field, operator: sqlOp, value: operand });
|
|
4481
|
+
}
|
|
4482
|
+
} else {
|
|
4483
|
+
this.iqo.having.push({ field, operator: "=", value });
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
return this;
|
|
4487
|
+
}
|
|
4488
|
+
sum(field) {
|
|
4489
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4490
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT COALESCE(SUM("${field}"), 0) as val FROM`);
|
|
4491
|
+
const results = this.executor(aggSql, params, true);
|
|
4492
|
+
return results[0]?.val ?? 0;
|
|
4493
|
+
}
|
|
4494
|
+
avg(field) {
|
|
4495
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4496
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT AVG("${field}") as val FROM`);
|
|
4497
|
+
const results = this.executor(aggSql, params, true);
|
|
4498
|
+
return results[0]?.val ?? 0;
|
|
4499
|
+
}
|
|
4500
|
+
min(field) {
|
|
4501
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4502
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT MIN("${field}") as val FROM`);
|
|
4503
|
+
const results = this.executor(aggSql, params, true);
|
|
4504
|
+
return results[0]?.val ?? null;
|
|
4505
|
+
}
|
|
4506
|
+
max(field) {
|
|
4507
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4508
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT MAX("${field}") as val FROM`);
|
|
4509
|
+
const results = this.executor(aggSql, params, true);
|
|
4510
|
+
return results[0]?.val ?? null;
|
|
4511
|
+
}
|
|
4512
|
+
paginate(page = 1, perPage = 20) {
|
|
4513
|
+
const total = this.count();
|
|
4514
|
+
const pages = Math.ceil(total / perPage);
|
|
4515
|
+
this.iqo.limit = perPage;
|
|
4516
|
+
this.iqo.offset = (page - 1) * perPage;
|
|
4517
|
+
const data = this.all();
|
|
4518
|
+
return { data, total, page, perPage, pages };
|
|
4519
|
+
}
|
|
4436
4520
|
then(onfulfilled, onrejected) {
|
|
4437
4521
|
try {
|
|
4438
4522
|
const result = this.all();
|
|
@@ -4638,6 +4722,8 @@ function executeProxyQuery(schemas, callback, executor) {
|
|
|
4638
4722
|
function createQueryBuilder(ctx, entityName, initialCols) {
|
|
4639
4723
|
const schema = ctx.schemas[entityName];
|
|
4640
4724
|
const executor = (sql, params, raw) => {
|
|
4725
|
+
if (ctx.debug)
|
|
4726
|
+
console.log("[satidb]", sql, params);
|
|
4641
4727
|
const rows = ctx.db.query(sql).all(...params);
|
|
4642
4728
|
if (raw)
|
|
4643
4729
|
return rows;
|
|
@@ -4707,6 +4793,9 @@ function createQueryBuilder(ctx, entityName, initialCols) {
|
|
|
4707
4793
|
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, eagerLoader);
|
|
4708
4794
|
if (initialCols.length > 0)
|
|
4709
4795
|
builder.select(...initialCols);
|
|
4796
|
+
if (ctx.softDeletes) {
|
|
4797
|
+
builder.where({ deletedAt: { $isNull: true } });
|
|
4798
|
+
}
|
|
4710
4799
|
return builder;
|
|
4711
4800
|
}
|
|
4712
4801
|
|
|
@@ -4771,6 +4860,14 @@ function buildWhereClause(conditions, tablePrefix) {
|
|
|
4771
4860
|
values.push(transformForStorage({ v: operand[0] }).v, transformForStorage({ v: operand[1] }).v);
|
|
4772
4861
|
continue;
|
|
4773
4862
|
}
|
|
4863
|
+
if (operator === "$isNull") {
|
|
4864
|
+
parts.push(`${fieldName} IS NULL`);
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
if (operator === "$isNotNull") {
|
|
4868
|
+
parts.push(`${fieldName} IS NOT NULL`);
|
|
4869
|
+
continue;
|
|
4870
|
+
}
|
|
4774
4871
|
const sqlOp = { $gt: ">", $gte: ">=", $lt: "<", $lte: "<=", $ne: "!=" }[operator];
|
|
4775
4872
|
if (!sqlOp)
|
|
4776
4873
|
throw new Error(`Unsupported operator '${operator}' on '${key}'`);
|
|
@@ -4807,9 +4904,16 @@ function insert(ctx, entityName, data) {
|
|
|
4807
4904
|
const schema = ctx.schemas[entityName];
|
|
4808
4905
|
const validatedData = asZodObject(schema).passthrough().parse(data);
|
|
4809
4906
|
const transformed = transformForStorage(validatedData);
|
|
4907
|
+
if (ctx.timestamps) {
|
|
4908
|
+
const now = new Date().toISOString();
|
|
4909
|
+
transformed.createdAt = now;
|
|
4910
|
+
transformed.updatedAt = now;
|
|
4911
|
+
}
|
|
4810
4912
|
const columns = Object.keys(transformed);
|
|
4811
4913
|
const quotedCols = columns.map((c) => `"${c}"`);
|
|
4812
4914
|
const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
4915
|
+
if (ctx.debug)
|
|
4916
|
+
console.log("[satidb]", sql, Object.values(transformed));
|
|
4813
4917
|
const result = ctx.db.query(sql).run(...Object.values(transformed));
|
|
4814
4918
|
const newEntity = getById(ctx, entityName, result.lastInsertRowid);
|
|
4815
4919
|
if (!newEntity)
|
|
@@ -4820,10 +4924,16 @@ function update(ctx, entityName, id, data) {
|
|
|
4820
4924
|
const schema = ctx.schemas[entityName];
|
|
4821
4925
|
const validatedData = asZodObject(schema).partial().parse(data);
|
|
4822
4926
|
const transformed = transformForStorage(validatedData);
|
|
4823
|
-
if (Object.keys(transformed).length === 0)
|
|
4927
|
+
if (Object.keys(transformed).length === 0 && !ctx.timestamps)
|
|
4824
4928
|
return getById(ctx, entityName, id);
|
|
4929
|
+
if (ctx.timestamps) {
|
|
4930
|
+
transformed.updatedAt = new Date().toISOString();
|
|
4931
|
+
}
|
|
4825
4932
|
const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
|
|
4826
|
-
|
|
4933
|
+
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
4934
|
+
if (ctx.debug)
|
|
4935
|
+
console.log("[satidb]", sql, [...Object.values(transformed), id]);
|
|
4936
|
+
ctx.db.query(sql).run(...Object.values(transformed), id);
|
|
4827
4937
|
return getById(ctx, entityName, id);
|
|
4828
4938
|
}
|
|
4829
4939
|
function updateWhere(ctx, entityName, data, conditions) {
|
|
@@ -4870,7 +4980,18 @@ function deleteWhere(ctx, entityName, conditions) {
|
|
|
4870
4980
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
4871
4981
|
if (!clause)
|
|
4872
4982
|
throw new Error("delete().where() requires at least one condition");
|
|
4873
|
-
|
|
4983
|
+
if (ctx.softDeletes) {
|
|
4984
|
+
const now = new Date().toISOString();
|
|
4985
|
+
const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
4986
|
+
if (ctx.debug)
|
|
4987
|
+
console.log("[satidb]", sql2, [now, ...values]);
|
|
4988
|
+
const result2 = ctx.db.query(sql2).run(now, ...values);
|
|
4989
|
+
return result2.changes ?? 0;
|
|
4990
|
+
}
|
|
4991
|
+
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
4992
|
+
if (ctx.debug)
|
|
4993
|
+
console.log("[satidb]", sql, values);
|
|
4994
|
+
const result = ctx.db.query(sql).run(...values);
|
|
4874
4995
|
return result.changes ?? 0;
|
|
4875
4996
|
}
|
|
4876
4997
|
function createDeleteBuilder(ctx, entityName) {
|
|
@@ -4894,6 +5015,11 @@ function insertMany(ctx, entityName, rows) {
|
|
|
4894
5015
|
for (const data of rows) {
|
|
4895
5016
|
const validatedData = zodSchema.parse(data);
|
|
4896
5017
|
const transformed = transformForStorage(validatedData);
|
|
5018
|
+
if (ctx.timestamps) {
|
|
5019
|
+
const now = new Date().toISOString();
|
|
5020
|
+
transformed.createdAt = now;
|
|
5021
|
+
transformed.updatedAt = now;
|
|
5022
|
+
}
|
|
4897
5023
|
const columns = Object.keys(transformed);
|
|
4898
5024
|
const quotedCols = columns.map((c) => `"${c}"`);
|
|
4899
5025
|
const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
@@ -4944,6 +5070,9 @@ function attachMethods(ctx, entityName, entity) {
|
|
|
4944
5070
|
class _Database {
|
|
4945
5071
|
db;
|
|
4946
5072
|
_reactive;
|
|
5073
|
+
_timestamps;
|
|
5074
|
+
_softDeletes;
|
|
5075
|
+
_debug;
|
|
4947
5076
|
schemas;
|
|
4948
5077
|
relationships;
|
|
4949
5078
|
options;
|
|
@@ -4959,6 +5088,9 @@ class _Database {
|
|
|
4959
5088
|
this.schemas = schemas;
|
|
4960
5089
|
this.options = options;
|
|
4961
5090
|
this._reactive = options.reactive !== false;
|
|
5091
|
+
this._timestamps = options.timestamps === true;
|
|
5092
|
+
this._softDeletes = options.softDeletes === true;
|
|
5093
|
+
this._debug = options.debug === true;
|
|
4962
5094
|
this._pollInterval = options.pollInterval ?? 100;
|
|
4963
5095
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
4964
5096
|
this._ctx = {
|
|
@@ -4966,7 +5098,10 @@ class _Database {
|
|
|
4966
5098
|
schemas: this.schemas,
|
|
4967
5099
|
relationships: this.relationships,
|
|
4968
5100
|
attachMethods: (name, entity) => attachMethods(this._ctx, name, entity),
|
|
4969
|
-
buildWhereClause: (conds, prefix) => buildWhereClause(conds, prefix)
|
|
5101
|
+
buildWhereClause: (conds, prefix) => buildWhereClause(conds, prefix),
|
|
5102
|
+
debug: this._debug,
|
|
5103
|
+
timestamps: this._timestamps,
|
|
5104
|
+
softDeletes: this._softDeletes
|
|
4970
5105
|
};
|
|
4971
5106
|
this.initializeTables();
|
|
4972
5107
|
if (this._reactive)
|
|
@@ -4974,6 +5109,8 @@ class _Database {
|
|
|
4974
5109
|
this.runMigrations();
|
|
4975
5110
|
if (options.indexes)
|
|
4976
5111
|
this.createIndexes(options.indexes);
|
|
5112
|
+
if (options.unique)
|
|
5113
|
+
this.createUniqueConstraints(options.unique);
|
|
4977
5114
|
for (const entityName of Object.keys(schemas)) {
|
|
4978
5115
|
const key = entityName;
|
|
4979
5116
|
const accessor = {
|
|
@@ -4986,10 +5123,23 @@ class _Database {
|
|
|
4986
5123
|
},
|
|
4987
5124
|
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
4988
5125
|
delete: (id) => {
|
|
4989
|
-
if (typeof id === "number")
|
|
5126
|
+
if (typeof id === "number") {
|
|
5127
|
+
if (this._softDeletes) {
|
|
5128
|
+
const now = new Date().toISOString();
|
|
5129
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
5130
|
+
return;
|
|
5131
|
+
}
|
|
4990
5132
|
return deleteEntity(this._ctx, entityName, id);
|
|
5133
|
+
}
|
|
4991
5134
|
return createDeleteBuilder(this._ctx, entityName);
|
|
4992
5135
|
},
|
|
5136
|
+
restore: (id) => {
|
|
5137
|
+
if (!this._softDeletes)
|
|
5138
|
+
throw new Error("restore() requires softDeletes: true");
|
|
5139
|
+
if (this._debug)
|
|
5140
|
+
console.log("[satidb]", `UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`, [id]);
|
|
5141
|
+
this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
5142
|
+
},
|
|
4993
5143
|
select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
|
|
4994
5144
|
on: (event, callback) => {
|
|
4995
5145
|
return this._registerListener(entityName, event, callback);
|
|
@@ -5003,6 +5153,13 @@ class _Database {
|
|
|
5003
5153
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
5004
5154
|
const storableFields = getStorableFields(schema);
|
|
5005
5155
|
const columnDefs = storableFields.map((f) => `"${f.name}" ${zodTypeToSqlType(f.type)}`);
|
|
5156
|
+
if (this._timestamps) {
|
|
5157
|
+
columnDefs.push('"createdAt" TEXT');
|
|
5158
|
+
columnDefs.push('"updatedAt" TEXT');
|
|
5159
|
+
}
|
|
5160
|
+
if (this._softDeletes) {
|
|
5161
|
+
columnDefs.push('"deletedAt" TEXT');
|
|
5162
|
+
}
|
|
5006
5163
|
const constraints = [];
|
|
5007
5164
|
const belongsToRels = this.relationships.filter((rel) => rel.type === "belongs-to" && rel.from === entityName);
|
|
5008
5165
|
for (const rel of belongsToRels) {
|
|
@@ -5062,6 +5219,14 @@ class _Database {
|
|
|
5062
5219
|
}
|
|
5063
5220
|
}
|
|
5064
5221
|
}
|
|
5222
|
+
createUniqueConstraints(unique) {
|
|
5223
|
+
for (const [tableName, groups] of Object.entries(unique)) {
|
|
5224
|
+
for (const cols of groups) {
|
|
5225
|
+
const idxName = `uq_${tableName}_${cols.join("_")}`;
|
|
5226
|
+
this.db.run(`CREATE UNIQUE INDEX IF NOT EXISTS "${idxName}" ON "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")})`);
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5065
5230
|
_registerListener(table, event, callback) {
|
|
5066
5231
|
if (!this._reactive) {
|
|
5067
5232
|
throw new Error("Change listeners are disabled. Set { reactive: true } (or omit it) in Database options to enable .on().");
|
|
@@ -5117,7 +5282,7 @@ class _Database {
|
|
|
5117
5282
|
}
|
|
5118
5283
|
this._changeWatermark = change.id;
|
|
5119
5284
|
}
|
|
5120
|
-
this.db.
|
|
5285
|
+
this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
|
|
5121
5286
|
}
|
|
5122
5287
|
transaction(callback) {
|
|
5123
5288
|
return this.db.transaction(callback)();
|
|
@@ -5127,7 +5292,27 @@ class _Database {
|
|
|
5127
5292
|
this.db.close();
|
|
5128
5293
|
}
|
|
5129
5294
|
query(callback) {
|
|
5130
|
-
return executeProxyQuery(this.schemas, callback, (sql, params) =>
|
|
5295
|
+
return executeProxyQuery(this.schemas, callback, (sql, params) => {
|
|
5296
|
+
if (this._debug)
|
|
5297
|
+
console.log("[satidb]", sql, params);
|
|
5298
|
+
return this.db.query(sql).all(...params);
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
raw(sql, ...params) {
|
|
5302
|
+
if (this._debug)
|
|
5303
|
+
console.log("[satidb]", sql, params);
|
|
5304
|
+
return this.db.query(sql).all(...params);
|
|
5305
|
+
}
|
|
5306
|
+
exec(sql, ...params) {
|
|
5307
|
+
if (this._debug)
|
|
5308
|
+
console.log("[satidb]", sql, params);
|
|
5309
|
+
this.db.run(sql, ...params);
|
|
5310
|
+
}
|
|
5311
|
+
tables() {
|
|
5312
|
+
return Object.keys(this.schemas);
|
|
5313
|
+
}
|
|
5314
|
+
columns(tableName) {
|
|
5315
|
+
return this.db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
5131
5316
|
}
|
|
5132
5317
|
}
|
|
5133
5318
|
var Database = _Database;
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -57,11 +57,13 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
57
57
|
whereAST: null,
|
|
58
58
|
joins: [],
|
|
59
59
|
groupBy: [],
|
|
60
|
+
having: [],
|
|
60
61
|
limit: null,
|
|
61
62
|
offset: null,
|
|
62
63
|
orderBy: [],
|
|
63
64
|
includes: [],
|
|
64
65
|
raw: false,
|
|
66
|
+
distinct: false,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -293,6 +295,105 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
293
295
|
return this;
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
/** Return only distinct rows. */
|
|
299
|
+
distinct(): this {
|
|
300
|
+
this.iqo.distinct = true;
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Include soft-deleted rows in query results.
|
|
306
|
+
* Only relevant when `softDeletes: true` is set in Database options.
|
|
307
|
+
*/
|
|
308
|
+
withTrashed(): this {
|
|
309
|
+
// Remove the auto-injected `deletedAt IS NULL` filter
|
|
310
|
+
this.iqo.wheres = this.iqo.wheres.filter(
|
|
311
|
+
w => !(w.field === 'deletedAt' && w.operator === 'IS NULL')
|
|
312
|
+
);
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Return only soft-deleted rows.
|
|
318
|
+
* Only relevant when `softDeletes: true` is set in Database options.
|
|
319
|
+
*/
|
|
320
|
+
onlyTrashed(): this {
|
|
321
|
+
// Remove the auto-injected `deletedAt IS NULL` and add `deletedAt IS NOT NULL`
|
|
322
|
+
this.iqo.wheres = this.iqo.wheres.filter(
|
|
323
|
+
w => !(w.field === 'deletedAt' && w.operator === 'IS NULL')
|
|
324
|
+
);
|
|
325
|
+
this.iqo.wheres.push({ field: 'deletedAt', operator: 'IS NOT NULL', value: null });
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Add HAVING conditions (used after groupBy for aggregate filtering).
|
|
331
|
+
*
|
|
332
|
+
* ```ts
|
|
333
|
+
* db.orders.select('user_id').groupBy('user_id')
|
|
334
|
+
* .having({ 'COUNT(*)': { $gt: 5 } })
|
|
335
|
+
* .raw().all()
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
having(conditions: Record<string, any>): this {
|
|
339
|
+
for (const [field, value] of Object.entries(conditions)) {
|
|
340
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
341
|
+
for (const [opKey, operand] of Object.entries(value)) {
|
|
342
|
+
const sqlOp = OPERATOR_MAP[opKey as WhereOperator];
|
|
343
|
+
if (!sqlOp) throw new Error(`Unsupported having operator: '${opKey}'`);
|
|
344
|
+
this.iqo.having.push({ field, operator: sqlOp as WhereCondition['operator'], value: operand });
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
this.iqo.having.push({ field, operator: '=', value });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------- Aggregate Methods ----------
|
|
354
|
+
|
|
355
|
+
/** Returns the SUM of a numeric column. */
|
|
356
|
+
sum(field: keyof T & string): number {
|
|
357
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
358
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT COALESCE(SUM("${field}"), 0) as val FROM`);
|
|
359
|
+
const results = this.executor(aggSql, params, true);
|
|
360
|
+
return (results[0] as any)?.val ?? 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Returns the AVG of a numeric column. */
|
|
364
|
+
avg(field: keyof T & string): number {
|
|
365
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
366
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT AVG("${field}") as val FROM`);
|
|
367
|
+
const results = this.executor(aggSql, params, true);
|
|
368
|
+
return (results[0] as any)?.val ?? 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Returns the MIN of a column. */
|
|
372
|
+
min(field: keyof T & string): number | string | null {
|
|
373
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
374
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT MIN("${field}") as val FROM`);
|
|
375
|
+
const results = this.executor(aggSql, params, true);
|
|
376
|
+
return (results[0] as any)?.val ?? null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Returns the MAX of a column. */
|
|
380
|
+
max(field: keyof T & string): number | string | null {
|
|
381
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
382
|
+
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT MAX("${field}") as val FROM`);
|
|
383
|
+
const results = this.executor(aggSql, params, true);
|
|
384
|
+
return (results[0] as any)?.val ?? null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Paginate results. Returns { data, total, page, perPage, pages }. */
|
|
388
|
+
paginate(page: number = 1, perPage: number = 20): { data: T[]; total: number; page: number; perPage: number; pages: number } {
|
|
389
|
+
const total = this.count();
|
|
390
|
+
const pages = Math.ceil(total / perPage);
|
|
391
|
+
this.iqo.limit = perPage;
|
|
392
|
+
this.iqo.offset = (page - 1) * perPage;
|
|
393
|
+
const data = this.all();
|
|
394
|
+
return { data, total, page, perPage, pages };
|
|
395
|
+
}
|
|
396
|
+
|
|
296
397
|
|
|
297
398
|
|
|
298
399
|
// ---------- Thenable (async/await support) ----------
|
package/src/context.ts
CHANGED
|
@@ -22,4 +22,13 @@ export interface DatabaseContext {
|
|
|
22
22
|
|
|
23
23
|
/** Build a WHERE clause from a conditions object. */
|
|
24
24
|
buildWhereClause(conditions: Record<string, any>, tablePrefix?: string): { clause: string; values: any[] };
|
|
25
|
+
|
|
26
|
+
/** Whether to log SQL to console. */
|
|
27
|
+
debug: boolean;
|
|
28
|
+
|
|
29
|
+
/** Whether tables have createdAt/updatedAt columns. */
|
|
30
|
+
timestamps: boolean;
|
|
31
|
+
|
|
32
|
+
/** Whether soft deletes are enabled (deletedAt column). */
|
|
33
|
+
softDeletes: boolean;
|
|
25
34
|
}
|
package/src/crud.ts
CHANGED
|
@@ -42,6 +42,14 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
42
42
|
const schema = ctx.schemas[entityName]!;
|
|
43
43
|
const validatedData = asZodObject(schema).passthrough().parse(data);
|
|
44
44
|
const transformed = transformForStorage(validatedData);
|
|
45
|
+
|
|
46
|
+
// Auto-inject timestamps
|
|
47
|
+
if (ctx.timestamps) {
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
transformed.createdAt = now;
|
|
50
|
+
transformed.updatedAt = now;
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
const columns = Object.keys(transformed);
|
|
46
54
|
|
|
47
55
|
const quotedCols = columns.map(c => `"${c}"`);
|
|
@@ -49,6 +57,7 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
49
57
|
? `INSERT INTO "${entityName}" DEFAULT VALUES`
|
|
50
58
|
: `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
|
|
51
59
|
|
|
60
|
+
if (ctx.debug) console.log('[satidb]', sql, Object.values(transformed));
|
|
52
61
|
const result = ctx.db.query(sql).run(...Object.values(transformed));
|
|
53
62
|
const newEntity = getById(ctx, entityName, result.lastInsertRowid as number);
|
|
54
63
|
if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
|
|
@@ -60,10 +69,17 @@ export function update<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
60
69
|
const schema = ctx.schemas[entityName]!;
|
|
61
70
|
const validatedData = asZodObject(schema).partial().parse(data);
|
|
62
71
|
const transformed = transformForStorage(validatedData);
|
|
63
|
-
if (Object.keys(transformed).length === 0) return getById(ctx, entityName, id);
|
|
72
|
+
if (Object.keys(transformed).length === 0 && !ctx.timestamps) return getById(ctx, entityName, id);
|
|
73
|
+
|
|
74
|
+
// Auto-update timestamp
|
|
75
|
+
if (ctx.timestamps) {
|
|
76
|
+
transformed.updatedAt = new Date().toISOString();
|
|
77
|
+
}
|
|
64
78
|
|
|
65
79
|
const setClause = Object.keys(transformed).map(key => `"${key}" = ?`).join(', ');
|
|
66
|
-
|
|
80
|
+
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
81
|
+
if (ctx.debug) console.log('[satidb]', sql, [...Object.values(transformed), id]);
|
|
82
|
+
ctx.db.query(sql).run(...Object.values(transformed), id);
|
|
67
83
|
|
|
68
84
|
return getById(ctx, entityName, id);
|
|
69
85
|
}
|
|
@@ -118,11 +134,23 @@ export function deleteEntity(ctx: DatabaseContext, entityName: string, id: numbe
|
|
|
118
134
|
ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
|
|
119
135
|
}
|
|
120
136
|
|
|
121
|
-
/** Delete all rows matching the given conditions. Returns the number of rows
|
|
137
|
+
/** Delete all rows matching the given conditions. Returns the number of rows affected. */
|
|
122
138
|
export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions: Record<string, any>): number {
|
|
123
139
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
124
140
|
if (!clause) throw new Error('delete().where() requires at least one condition');
|
|
125
|
-
|
|
141
|
+
|
|
142
|
+
if (ctx.softDeletes) {
|
|
143
|
+
// Soft delete: set deletedAt instead of removing rows
|
|
144
|
+
const now = new Date().toISOString();
|
|
145
|
+
const sql = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
146
|
+
if (ctx.debug) console.log('[satidb]', sql, [now, ...values]);
|
|
147
|
+
const result = ctx.db.query(sql).run(now, ...values);
|
|
148
|
+
return (result as any).changes ?? 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
152
|
+
if (ctx.debug) console.log('[satidb]', sql, values);
|
|
153
|
+
const result = ctx.db.query(sql).run(...values);
|
|
126
154
|
return (result as any).changes ?? 0;
|
|
127
155
|
}
|
|
128
156
|
|
|
@@ -147,6 +175,13 @@ export function insertMany<T extends Record<string, any>>(ctx: DatabaseContext,
|
|
|
147
175
|
for (const data of rows) {
|
|
148
176
|
const validatedData = zodSchema.parse(data);
|
|
149
177
|
const transformed = transformForStorage(validatedData);
|
|
178
|
+
|
|
179
|
+
if (ctx.timestamps) {
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
transformed.createdAt = now;
|
|
182
|
+
transformed.updatedAt = now;
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
const columns = Object.keys(transformed);
|
|
151
186
|
const quotedCols = columns.map(c => `"${c}"`);
|
|
152
187
|
const sql = columns.length === 0
|