@veap/database 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +176 -2
- package/dist/casts.d.ts +9 -0
- package/dist/casts.js +78 -0
- package/dist/connection.d.ts +49 -0
- package/dist/connection.js +189 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +7 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +161 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +21 -0
- package/dist/migration-provider.d.ts +5 -0
- package/dist/migration-provider.js +15 -0
- package/dist/migrations/generator.d.ts +4 -0
- package/dist/migrations/generator.js +58 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/runner.d.ts +12 -0
- package/dist/migrations/runner.js +178 -0
- package/dist/migrations/schema-builder.d.ts +84 -0
- package/dist/migrations/schema-builder.js +409 -0
- package/dist/model.d.ts +379 -0
- package/dist/model.js +899 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.js +58 -0
- package/dist/query-builder.d.ts +197 -0
- package/dist/query-builder.js +715 -0
- package/dist/relations/belongs-to-many.d.ts +15 -0
- package/dist/relations/belongs-to-many.js +80 -0
- package/dist/relations/belongs-to.d.ts +8 -0
- package/dist/relations/belongs-to.js +37 -0
- package/dist/relations/eager-loader.d.ts +5 -0
- package/dist/relations/eager-loader.js +66 -0
- package/dist/relations/has-many.d.ts +9 -0
- package/dist/relations/has-many.js +48 -0
- package/dist/relations/has-one.d.ts +8 -0
- package/dist/relations/has-one.js +37 -0
- package/dist/relations/index.d.ts +11 -0
- package/dist/relations/index.js +11 -0
- package/dist/relations/morph-many.d.ts +14 -0
- package/dist/relations/morph-many.js +65 -0
- package/dist/relations/morph-map.d.ts +32 -0
- package/dist/relations/morph-map.js +73 -0
- package/dist/relations/morph-one.d.ts +14 -0
- package/dist/relations/morph-one.js +65 -0
- package/dist/relations/morph-to-many.d.ts +44 -0
- package/dist/relations/morph-to-many.js +215 -0
- package/dist/relations/morph-to.d.ts +19 -0
- package/dist/relations/morph-to.js +90 -0
- package/dist/relations/relation.d.ts +22 -0
- package/dist/relations/relation.js +13 -0
- package/dist/scopes.d.ts +16 -0
- package/dist/scopes.js +1 -0
- package/dist/seeder.d.ts +25 -0
- package/dist/seeder.js +49 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +20 -0
- package/package.json +50 -4
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { getKnex } from "../connection";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight SQL helper for migrations, compatible with sql`...` template strings.
|
|
4
|
+
*/
|
|
5
|
+
export function sql(strings, ...values) {
|
|
6
|
+
if (typeof strings === "string")
|
|
7
|
+
return strings;
|
|
8
|
+
let result = strings[0];
|
|
9
|
+
for (let i = 0; i < values.length; i++) {
|
|
10
|
+
result += values[i] + strings[i + 1];
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
sql.raw = (str) => str;
|
|
15
|
+
export class ColumnBuilder {
|
|
16
|
+
type;
|
|
17
|
+
name;
|
|
18
|
+
_notNull = false;
|
|
19
|
+
_primaryKey = false;
|
|
20
|
+
_unique = false;
|
|
21
|
+
_default = null;
|
|
22
|
+
_defaultNow = false;
|
|
23
|
+
_references = null;
|
|
24
|
+
_onDelete = null;
|
|
25
|
+
_onUpdate = null;
|
|
26
|
+
constructor(type, name) {
|
|
27
|
+
this.type = type;
|
|
28
|
+
this.name = name;
|
|
29
|
+
}
|
|
30
|
+
notNull() {
|
|
31
|
+
this._notNull = true;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
primaryKey() {
|
|
35
|
+
this._primaryKey = true;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
unique() {
|
|
39
|
+
this._unique = true;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
default(value) {
|
|
43
|
+
this._default = value;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
defaultNow() {
|
|
47
|
+
this._defaultNow = true;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
defaultFn(fn) {
|
|
51
|
+
// If it's a known function or common pattern, map to SQL
|
|
52
|
+
const fnStr = fn.toString().toLowerCase();
|
|
53
|
+
if (fnStr.includes("randomuuid") ||
|
|
54
|
+
fnStr.includes("uuidv4") ||
|
|
55
|
+
fnStr.includes("gen_random_uuid")) {
|
|
56
|
+
this._default = "gen_random_uuid()";
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
references(table, column = "id") {
|
|
61
|
+
this._references = { table, column };
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
onDelete(action) {
|
|
65
|
+
this._onDelete = action;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
onUpdate(action) {
|
|
69
|
+
this._onUpdate = action;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
toSql(dialect = "postgres") {
|
|
73
|
+
const isSqlite = dialect === "sqlite";
|
|
74
|
+
// Map types for SQLite if needed
|
|
75
|
+
let colType = this.type;
|
|
76
|
+
if (isSqlite) {
|
|
77
|
+
if (colType === "BYTEA") {
|
|
78
|
+
colType = "BLOB";
|
|
79
|
+
}
|
|
80
|
+
else if (colType.startsWith("TIMESTAMP") ||
|
|
81
|
+
colType.startsWith("DATE") ||
|
|
82
|
+
colType.startsWith("VARCHAR") ||
|
|
83
|
+
colType === "UUID") {
|
|
84
|
+
colType = "TEXT";
|
|
85
|
+
}
|
|
86
|
+
else if (colType === "JSONB" || colType === "JSON") {
|
|
87
|
+
colType = "TEXT";
|
|
88
|
+
}
|
|
89
|
+
else if (colType === "BOOLEAN") {
|
|
90
|
+
colType = "INTEGER";
|
|
91
|
+
}
|
|
92
|
+
else if (colType === "SERIAL") {
|
|
93
|
+
colType = "INTEGER";
|
|
94
|
+
}
|
|
95
|
+
else if (colType === "DOUBLE PRECISION" || colType === "DECIMAL") {
|
|
96
|
+
colType = "NUMERIC";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let sql = `"${this.name}" ${colType}`;
|
|
100
|
+
if (this._primaryKey) {
|
|
101
|
+
if (isSqlite && this.type === "SERIAL") {
|
|
102
|
+
sql = `"${this.name}" INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
103
|
+
return sql;
|
|
104
|
+
}
|
|
105
|
+
sql += " PRIMARY KEY";
|
|
106
|
+
}
|
|
107
|
+
if (this._notNull)
|
|
108
|
+
sql += " NOT NULL";
|
|
109
|
+
if (this._unique)
|
|
110
|
+
sql += " UNIQUE";
|
|
111
|
+
if (this._defaultNow) {
|
|
112
|
+
sql += isSqlite ? " DEFAULT CURRENT_TIMESTAMP" : " DEFAULT NOW()";
|
|
113
|
+
}
|
|
114
|
+
else if (this._default !== null) {
|
|
115
|
+
if (typeof this._default === "string" && this._default.includes("()")) {
|
|
116
|
+
if (isSqlite && this._default === "gen_random_uuid()") {
|
|
117
|
+
// SQLite doesn't have native gen_random_uuid() built-in by default without extensions
|
|
118
|
+
// Handled via application-level autoUuid
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
sql += ` DEFAULT ${this._default}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (typeof this._default === "boolean") {
|
|
125
|
+
if (isSqlite) {
|
|
126
|
+
sql += ` DEFAULT ${this._default ? "1" : "0"}`;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
sql += ` DEFAULT ${this._default ? "TRUE" : "FALSE"}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const val = typeof this._default === "string"
|
|
134
|
+
? `'${this._default}'`
|
|
135
|
+
: this._default;
|
|
136
|
+
sql += ` DEFAULT ${val}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (this._references) {
|
|
140
|
+
sql += ` REFERENCES "${this._references.table}"("${this._references.column}")`;
|
|
141
|
+
if (this._onDelete) {
|
|
142
|
+
sql += ` ON DELETE ${this._onDelete}`;
|
|
143
|
+
}
|
|
144
|
+
if (this._onUpdate) {
|
|
145
|
+
sql += ` ON UPDATE ${this._onUpdate}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return sql;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export class Blueprint {
|
|
152
|
+
tableName;
|
|
153
|
+
columns = [];
|
|
154
|
+
_primaryKeys = [];
|
|
155
|
+
_uniques = [];
|
|
156
|
+
constructor(tableName) {
|
|
157
|
+
this.tableName = tableName;
|
|
158
|
+
}
|
|
159
|
+
id(name = "id") {
|
|
160
|
+
return this.addColumn("SERIAL", name).primaryKey();
|
|
161
|
+
}
|
|
162
|
+
uuid(name = "id") {
|
|
163
|
+
return this.addColumn("UUID", name);
|
|
164
|
+
}
|
|
165
|
+
increments(name) {
|
|
166
|
+
return this.addColumn("SERIAL", name).primaryKey();
|
|
167
|
+
}
|
|
168
|
+
text(name) {
|
|
169
|
+
return this.addColumn("TEXT", name);
|
|
170
|
+
}
|
|
171
|
+
varchar(name, length = 255) {
|
|
172
|
+
return this.addColumn(`VARCHAR(${length})`, name);
|
|
173
|
+
}
|
|
174
|
+
string(name, length = 255) {
|
|
175
|
+
return this.varchar(name, length);
|
|
176
|
+
}
|
|
177
|
+
integer(name) {
|
|
178
|
+
return this.addColumn("INTEGER", name);
|
|
179
|
+
}
|
|
180
|
+
smallint(name) {
|
|
181
|
+
return this.addColumn("SMALLINT", name);
|
|
182
|
+
}
|
|
183
|
+
bigint(name) {
|
|
184
|
+
return this.addColumn("BIGINT", name);
|
|
185
|
+
}
|
|
186
|
+
boolean(name) {
|
|
187
|
+
return this.addColumn("BOOLEAN", name);
|
|
188
|
+
}
|
|
189
|
+
timestamp(name, precision) {
|
|
190
|
+
const type = precision !== undefined ? `TIMESTAMP(${precision})` : "TIMESTAMP";
|
|
191
|
+
return this.addColumn(type, name);
|
|
192
|
+
}
|
|
193
|
+
timestamptz(name, precision) {
|
|
194
|
+
const type = precision !== undefined
|
|
195
|
+
? `TIMESTAMP(${precision}) WITH TIME ZONE`
|
|
196
|
+
: "TIMESTAMP WITH TIME ZONE";
|
|
197
|
+
return this.addColumn(type, name);
|
|
198
|
+
}
|
|
199
|
+
timestamps() {
|
|
200
|
+
this.timestamp("created_at").defaultNow().notNull();
|
|
201
|
+
this.timestamp("updated_at").defaultNow().notNull();
|
|
202
|
+
}
|
|
203
|
+
softDeletes(name = "deleted_at") {
|
|
204
|
+
return this.timestamp(name);
|
|
205
|
+
}
|
|
206
|
+
date(name) {
|
|
207
|
+
return this.addColumn("DATE", name);
|
|
208
|
+
}
|
|
209
|
+
json(name) {
|
|
210
|
+
return this.addColumn("JSONB", name);
|
|
211
|
+
}
|
|
212
|
+
jsonb(name) {
|
|
213
|
+
return this.addColumn("JSONB", name);
|
|
214
|
+
}
|
|
215
|
+
bytea(name) {
|
|
216
|
+
return this.addColumn("BYTEA", name);
|
|
217
|
+
}
|
|
218
|
+
blob(name) {
|
|
219
|
+
return this.addColumn("BYTEA", name);
|
|
220
|
+
}
|
|
221
|
+
decimal(name, precision, scale) {
|
|
222
|
+
const type = precision !== undefined && scale !== undefined
|
|
223
|
+
? `DECIMAL(${precision}, ${scale})`
|
|
224
|
+
: "DECIMAL";
|
|
225
|
+
return this.addColumn(type, name);
|
|
226
|
+
}
|
|
227
|
+
float(name) {
|
|
228
|
+
return this.addColumn("REAL", name);
|
|
229
|
+
}
|
|
230
|
+
double(name) {
|
|
231
|
+
return this.addColumn("DOUBLE PRECISION", name);
|
|
232
|
+
}
|
|
233
|
+
custom(type, name) {
|
|
234
|
+
return this.addColumn(type, name);
|
|
235
|
+
}
|
|
236
|
+
unique(columns) {
|
|
237
|
+
this._uniques.push(columns);
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
primary(columns) {
|
|
241
|
+
this._primaryKeys = columns;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
addColumn(type, name) {
|
|
245
|
+
const builder = new ColumnBuilder(type, name);
|
|
246
|
+
this.columns.push(builder);
|
|
247
|
+
return builder;
|
|
248
|
+
}
|
|
249
|
+
toSql(dialect = "postgres") {
|
|
250
|
+
const isSqlite = dialect === "sqlite";
|
|
251
|
+
const columnDefs = this.columns.map((c) => c.toSql(dialect));
|
|
252
|
+
if (this._primaryKeys.length > 0) {
|
|
253
|
+
if (isSqlite) {
|
|
254
|
+
columnDefs.push(`PRIMARY KEY("${this._primaryKeys.join('", "')}")`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const pkName = `${this.tableName}_${this._primaryKeys.join("_")}_pk`;
|
|
258
|
+
columnDefs.push(`CONSTRAINT "${pkName}" PRIMARY KEY("${this._primaryKeys.join('", "')}")`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const uniqueCols of this._uniques) {
|
|
262
|
+
if (isSqlite) {
|
|
263
|
+
columnDefs.push(`UNIQUE("${uniqueCols.join('", "')}")`);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const uName = `${this.tableName}_${uniqueCols.join("_")}_uq`;
|
|
267
|
+
columnDefs.push(`CONSTRAINT "${uName}" UNIQUE("${uniqueCols.join('", "')}")`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return `CREATE TABLE IF NOT EXISTS "${this.tableName}" (${columnDefs.join(", ")})`;
|
|
271
|
+
}
|
|
272
|
+
toAlterSql(dialect = "postgres") {
|
|
273
|
+
const isSqlite = dialect === "sqlite";
|
|
274
|
+
const sqls = this.columns.map((c) => `ALTER TABLE "${this.tableName}" ADD COLUMN ${isSqlite ? "" : "IF NOT EXISTS "}${c.toSql(dialect)}`);
|
|
275
|
+
for (const uniqueCols of this._uniques) {
|
|
276
|
+
if (isSqlite) {
|
|
277
|
+
const uName = `${this.tableName}_${uniqueCols.join("_")}_uq_idx`;
|
|
278
|
+
sqls.push(`CREATE UNIQUE INDEX IF NOT EXISTS "${uName}" ON "${this.tableName}" ("${uniqueCols.join('", "')}")`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const uName = `${this.tableName}_${uniqueCols.join("_")}_uq`;
|
|
282
|
+
// Use DO block for idempotent constraint addition in Postgres
|
|
283
|
+
sqls.push(`
|
|
284
|
+
DO $$ BEGIN
|
|
285
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '${uName}') THEN
|
|
286
|
+
ALTER TABLE "${this.tableName}" ADD CONSTRAINT "${uName}" UNIQUE("${uniqueCols.join('", "')}");
|
|
287
|
+
END IF;
|
|
288
|
+
END $$;
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return sqls;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export class Schema {
|
|
296
|
+
db;
|
|
297
|
+
constructor(db) {
|
|
298
|
+
this.db = db;
|
|
299
|
+
}
|
|
300
|
+
getDialect() {
|
|
301
|
+
const knex = this.getKnexInstance();
|
|
302
|
+
const clientName = knex?.client?.config?.client || "";
|
|
303
|
+
if (clientName.includes("sqlite")) {
|
|
304
|
+
return "sqlite";
|
|
305
|
+
}
|
|
306
|
+
if (clientName.includes("mysql")) {
|
|
307
|
+
return "mysql";
|
|
308
|
+
}
|
|
309
|
+
return "postgres";
|
|
310
|
+
}
|
|
311
|
+
get knex() {
|
|
312
|
+
return this.getKnexInstance();
|
|
313
|
+
}
|
|
314
|
+
getKnexInstance() {
|
|
315
|
+
if (this.db?.client)
|
|
316
|
+
return this.db;
|
|
317
|
+
return getKnex();
|
|
318
|
+
}
|
|
319
|
+
async hasTable(name) {
|
|
320
|
+
const knex = this.getKnexInstance();
|
|
321
|
+
return await knex.schema.hasTable(name);
|
|
322
|
+
}
|
|
323
|
+
async hasColumn(tableName, columnName) {
|
|
324
|
+
const dialect = this.getDialect();
|
|
325
|
+
const knex = this.getKnexInstance();
|
|
326
|
+
if (dialect === "sqlite") {
|
|
327
|
+
const result = await knex.raw(`PRAGMA table_info("${tableName}")`);
|
|
328
|
+
const rows = Array.isArray(result) ? result : result?.rows || [];
|
|
329
|
+
return rows.some((r) => r.name === columnName);
|
|
330
|
+
}
|
|
331
|
+
return await knex.schema.hasColumn(tableName, columnName);
|
|
332
|
+
}
|
|
333
|
+
async dropColumn(tableName, columnName) {
|
|
334
|
+
const dialect = this.getDialect();
|
|
335
|
+
const hasCol = await this.hasColumn(tableName, columnName);
|
|
336
|
+
if (!hasCol)
|
|
337
|
+
return;
|
|
338
|
+
if (dialect === "sqlite") {
|
|
339
|
+
// SQLite rejects ALTER TABLE ... DROP COLUMN for UNIQUE/indexed columns.
|
|
340
|
+
// Knex handles the required table recreation automatically and safely.
|
|
341
|
+
const knex = this.getKnexInstance();
|
|
342
|
+
await knex.schema.alterTable(tableName, (table) => {
|
|
343
|
+
table.dropColumn(columnName);
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await this.runQuery(`ALTER TABLE "${tableName}" DROP COLUMN IF EXISTS "${columnName}"`);
|
|
348
|
+
}
|
|
349
|
+
async runQuery(query) {
|
|
350
|
+
if (typeof this.db?.raw === "function") {
|
|
351
|
+
await this.db.raw(query);
|
|
352
|
+
}
|
|
353
|
+
else if (typeof this.db?.execute === "function") {
|
|
354
|
+
await this.db.execute(query);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const knex = getKnex();
|
|
358
|
+
await knex.raw(query);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async createTable(name, callback) {
|
|
362
|
+
const blueprint = new Blueprint(name);
|
|
363
|
+
callback(blueprint);
|
|
364
|
+
const dialect = this.getDialect();
|
|
365
|
+
const query = blueprint.toSql(dialect);
|
|
366
|
+
await this.runQuery(query);
|
|
367
|
+
}
|
|
368
|
+
async table(name, callback) {
|
|
369
|
+
const blueprint = new Blueprint(name);
|
|
370
|
+
callback(blueprint);
|
|
371
|
+
const dialect = this.getDialect();
|
|
372
|
+
const queries = blueprint.toAlterSql(dialect);
|
|
373
|
+
for (const query of queries) {
|
|
374
|
+
await this.runQuery(query);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async dropTable(name) {
|
|
378
|
+
const dialect = this.getDialect();
|
|
379
|
+
const cascade = dialect === "sqlite" ? "" : " CASCADE";
|
|
380
|
+
await this.runQuery(`DROP TABLE IF EXISTS "${name}"${cascade}`);
|
|
381
|
+
}
|
|
382
|
+
async dropTableIfExists(name) {
|
|
383
|
+
await this.dropTable(name);
|
|
384
|
+
}
|
|
385
|
+
async createEnum(name, values) {
|
|
386
|
+
const dialect = this.getDialect();
|
|
387
|
+
if (dialect === "sqlite") {
|
|
388
|
+
// SQLite does not support standalone ENUM types; columns use TEXT with check constraints
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const vals = values.map((v) => `'${v}'`).join(", ");
|
|
392
|
+
await this.runQuery(`
|
|
393
|
+
DO $$ BEGIN
|
|
394
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '${name}') THEN
|
|
395
|
+
CREATE TYPE "${name}" AS ENUM (${vals});
|
|
396
|
+
END IF;
|
|
397
|
+
END $$;
|
|
398
|
+
`);
|
|
399
|
+
}
|
|
400
|
+
async dropEnum(name) {
|
|
401
|
+
const dialect = this.getDialect();
|
|
402
|
+
if (dialect === "sqlite")
|
|
403
|
+
return;
|
|
404
|
+
await this.runQuery(`DROP TYPE IF EXISTS "${name}" CASCADE`);
|
|
405
|
+
}
|
|
406
|
+
async raw(query) {
|
|
407
|
+
await this.runQuery(query);
|
|
408
|
+
}
|
|
409
|
+
}
|