@rwillians/exto 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bun-postgres.js +75 -0
- package/dist/cjs/bun-sqlite.js +94 -0
- package/dist/cjs/index.js +453 -0
- package/dist/cjs/standard-schema.js +240 -0
- package/dist/cjs/utils.js +17 -0
- package/dist/esm/bun-postgres.js +71 -0
- package/dist/esm/bun-sqlite.js +90 -0
- package/dist/esm/index.js +408 -0
- package/dist/esm/standard-schema.js +229 -0
- package/dist/esm/utils.js +10 -0
- package/dist/types/bun-postgres.d.ts +16 -0
- package/dist/types/bun-sqlite.d.ts +16 -0
- package/dist/types/index.d.ts +315 -0
- package/dist/types/standard-schema.d.ts +174 -0
- package/dist/types/utils.d.ts +11 -0
- package/package.json +6 -6
- package/dist/.gitignore +0 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dialect = exports.connect = void 0;
|
|
4
|
+
const bun_1 = require("bun");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const TYPES = {
|
|
7
|
+
BIGINT: () => 'BIGINT',
|
|
8
|
+
BINARY: () => 'BYTEA',
|
|
9
|
+
BOOLEAN: () => 'BOOLEAN',
|
|
10
|
+
CITEXT: () => 'CITEXT',
|
|
11
|
+
DATE: () => 'DATE',
|
|
12
|
+
DATETIME: () => 'TIMESTAMPTZ',
|
|
13
|
+
DECIMAL: ({ precision, scale }) => [precision, scale].every(Number.isInteger) ? `NUMERIC(${precision}, ${scale})`
|
|
14
|
+
: [precision, scale].some(Number.isInteger) ? (0, utils_1.raise)('precision and scale are mutually inclusive, either you provide both or neither')
|
|
15
|
+
: 'NUMERIC',
|
|
16
|
+
FLOAT: () => 'DOUBLE PRECISION',
|
|
17
|
+
INTEGER: () => 'BIGINT',
|
|
18
|
+
JSON: () => 'JSONB',
|
|
19
|
+
SEQUENCE: () => 'BIGINT',
|
|
20
|
+
STRING: ({ length = 255 }) => `VARCHAR(${length})`,
|
|
21
|
+
TEXT: () => 'TEXT',
|
|
22
|
+
UUID: () => 'UUID',
|
|
23
|
+
};
|
|
24
|
+
const identity = () => ({
|
|
25
|
+
encode: (value) => value,
|
|
26
|
+
decode: (value) => value,
|
|
27
|
+
});
|
|
28
|
+
const dialect = {
|
|
29
|
+
name: 'postgres',
|
|
30
|
+
placeholder: (index) => `$${index}`,
|
|
31
|
+
type: (col) => TYPES[col.type](col),
|
|
32
|
+
generated: (col) => col.type === 'SEQUENCE'
|
|
33
|
+
? 'GENERATED ALWAYS AS IDENTITY'
|
|
34
|
+
: undefined,
|
|
35
|
+
codecs: {
|
|
36
|
+
BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
|
|
37
|
+
BINARY: identity(),
|
|
38
|
+
BOOLEAN: identity(),
|
|
39
|
+
CITEXT: identity(),
|
|
40
|
+
DATE: { encode: (value) => value, decode: (value) => value instanceof Date ? value.toISOString().slice(0, 10) : value },
|
|
41
|
+
DATETIME: identity(),
|
|
42
|
+
DECIMAL: { encode: (value) => value, decode: (value) => Number(value) },
|
|
43
|
+
FLOAT: identity(),
|
|
44
|
+
INTEGER: { encode: (value) => value, decode: (value) => Number(value) },
|
|
45
|
+
JSON: identity(),
|
|
46
|
+
SEQUENCE: { encode: (value) => value, decode: (value) => Number(value) },
|
|
47
|
+
STRING: identity(),
|
|
48
|
+
TEXT: identity(),
|
|
49
|
+
UUID: identity(),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
exports.dialect = dialect;
|
|
53
|
+
class BunPostgres {
|
|
54
|
+
sql;
|
|
55
|
+
loggers;
|
|
56
|
+
inTransaction;
|
|
57
|
+
dialect = dialect;
|
|
58
|
+
constructor(sql, loggers = [], inTransaction = false) {
|
|
59
|
+
this.sql = sql;
|
|
60
|
+
this.loggers = loggers;
|
|
61
|
+
this.inTransaction = inTransaction;
|
|
62
|
+
}
|
|
63
|
+
async execute(sql, params) {
|
|
64
|
+
return await this.sql.unsafe(sql, params);
|
|
65
|
+
}
|
|
66
|
+
async transaction(fn) {
|
|
67
|
+
const callback = (tx) => fn(new BunPostgres(tx, this.loggers, true));
|
|
68
|
+
const result = this.inTransaction
|
|
69
|
+
? this.sql.savepoint(callback)
|
|
70
|
+
: this.sql.begin(callback);
|
|
71
|
+
return await result;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const connect = (url, { loggers = [] } = {}) => new BunPostgres(new bun_1.SQL(url), loggers);
|
|
75
|
+
exports.connect = connect;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dialect = exports.connect = void 0;
|
|
4
|
+
const bun_sqlite_1 = require("bun:sqlite");
|
|
5
|
+
const TYPES = {
|
|
6
|
+
BIGINT: 'INTEGER',
|
|
7
|
+
BINARY: 'BLOB',
|
|
8
|
+
BOOLEAN: 'INTEGER',
|
|
9
|
+
CITEXT: 'TEXT COLLATE NOCASE',
|
|
10
|
+
DATE: 'TEXT',
|
|
11
|
+
DATETIME: 'INTEGER',
|
|
12
|
+
DECIMAL: 'NUMERIC',
|
|
13
|
+
FLOAT: 'REAL',
|
|
14
|
+
INTEGER: 'INTEGER',
|
|
15
|
+
JSON: 'TEXT',
|
|
16
|
+
SEQUENCE: 'INTEGER',
|
|
17
|
+
STRING: 'TEXT',
|
|
18
|
+
TEXT: 'TEXT',
|
|
19
|
+
UUID: 'TEXT',
|
|
20
|
+
};
|
|
21
|
+
const identity = () => ({
|
|
22
|
+
encode: (value) => value,
|
|
23
|
+
decode: (value) => value,
|
|
24
|
+
});
|
|
25
|
+
// integers are read as bigints (see `execute`), so those that aren't BIGINT
|
|
26
|
+
// columns go back to being numbers
|
|
27
|
+
const numeric = {
|
|
28
|
+
encode: (value) => value,
|
|
29
|
+
decode: (value) => Number(value),
|
|
30
|
+
};
|
|
31
|
+
const dialect = {
|
|
32
|
+
name: 'sqlite',
|
|
33
|
+
placeholder: () => '?',
|
|
34
|
+
type: (col) => TYPES[col.type],
|
|
35
|
+
generated: (col) => {
|
|
36
|
+
if (col.type === 'SEQUENCE' && col.primaryKey)
|
|
37
|
+
return 'AUTOINCREMENT';
|
|
38
|
+
if (col.type === 'SEQUENCE')
|
|
39
|
+
throw new Error(`SQLite can only generate sequences for primary keys, column "${col.name}" isn't one`);
|
|
40
|
+
return undefined;
|
|
41
|
+
},
|
|
42
|
+
codecs: {
|
|
43
|
+
BIGINT: { encode: (value) => value, decode: (value) => BigInt(value) },
|
|
44
|
+
BINARY: identity(),
|
|
45
|
+
BOOLEAN: { encode: (value) => (value ? 1 : 0), decode: (value) => Number(value) === 1 },
|
|
46
|
+
CITEXT: identity(),
|
|
47
|
+
DATE: identity(),
|
|
48
|
+
DATETIME: { encode: (value) => value.getTime(), decode: (value) => new Date(Number(value)) },
|
|
49
|
+
DECIMAL: numeric,
|
|
50
|
+
FLOAT: numeric,
|
|
51
|
+
INTEGER: numeric,
|
|
52
|
+
JSON: { encode: (value) => JSON.stringify(value), decode: (value) => JSON.parse(value) },
|
|
53
|
+
SEQUENCE: numeric,
|
|
54
|
+
STRING: identity(),
|
|
55
|
+
TEXT: identity(),
|
|
56
|
+
UUID: identity(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
exports.dialect = dialect;
|
|
60
|
+
class BunSQLite {
|
|
61
|
+
conn;
|
|
62
|
+
loggers;
|
|
63
|
+
depth;
|
|
64
|
+
dialect = dialect;
|
|
65
|
+
constructor(conn, loggers = [], depth = 0) {
|
|
66
|
+
this.conn = conn;
|
|
67
|
+
this.loggers = loggers;
|
|
68
|
+
this.depth = depth;
|
|
69
|
+
}
|
|
70
|
+
async execute(sql, params) {
|
|
71
|
+
// without safe integers, bun:sqlite rounds anything beyond 2^53
|
|
72
|
+
return this.conn.prepare(sql).safeIntegers(true).all(...params);
|
|
73
|
+
}
|
|
74
|
+
// bun:sqlite's own `transaction()` is synchronous and would commit before an
|
|
75
|
+
// async callback settles, hence the manual BEGIN/COMMIT (SAVEPOINT if nested)
|
|
76
|
+
async transaction(fn) {
|
|
77
|
+
const savepoint = `"sp_${this.depth}"`;
|
|
78
|
+
const [begin, commit, rollback] = this.depth === 0
|
|
79
|
+
? ['BEGIN', 'COMMIT', 'ROLLBACK']
|
|
80
|
+
: [`SAVEPOINT ${savepoint}`, `RELEASE ${savepoint}`, `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`];
|
|
81
|
+
this.conn.run(begin);
|
|
82
|
+
try {
|
|
83
|
+
const result = await fn(new BunSQLite(this.conn, this.loggers, this.depth + 1));
|
|
84
|
+
this.conn.run(commit);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.conn.run(rollback);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const connect = (filename = ':memory:', { loggers = [] } = {}) => new BunSQLite(new bun_sqlite_1.Database(filename), loggers);
|
|
94
|
+
exports.connect = connect;
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.t = exports.into = exports.from = exports.expr = exports.exec = exports.table = exports.defineMigrations = exports.create = exports.column = exports.ValidationError = void 0;
|
|
37
|
+
const std = __importStar(require("./standard-schema"));
|
|
38
|
+
const standard_schema_1 = require("./standard-schema");
|
|
39
|
+
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return standard_schema_1.ValidationError; } });
|
|
40
|
+
const utils_1 = require("./utils");
|
|
41
|
+
class ColumnBuilder {
|
|
42
|
+
props;
|
|
43
|
+
constructor(props) {
|
|
44
|
+
this.props = props;
|
|
45
|
+
}
|
|
46
|
+
default(value) {
|
|
47
|
+
return new ColumnBuilder({ ...this.props, default: value });
|
|
48
|
+
}
|
|
49
|
+
generated() {
|
|
50
|
+
if (this.props.type !== 'UUID')
|
|
51
|
+
throw new Error('Only UUID columns can be generated, use t.sequence() for integers');
|
|
52
|
+
return new ColumnBuilder({ ...this.props, generated: true });
|
|
53
|
+
}
|
|
54
|
+
nullable() {
|
|
55
|
+
if (this.props.primaryKey)
|
|
56
|
+
throw new Error('Cannot make a primary key column nullable');
|
|
57
|
+
return new ColumnBuilder({ ...this.props, nullable: true, schema: std.nullable(this.props.schema) });
|
|
58
|
+
}
|
|
59
|
+
primaryKey() {
|
|
60
|
+
if (this.props.nullable)
|
|
61
|
+
throw new Error('Cannot make a nullable column a primary key');
|
|
62
|
+
return new ColumnBuilder({ ...this.props, primaryKey: true });
|
|
63
|
+
}
|
|
64
|
+
unique() {
|
|
65
|
+
if (this.props.primaryKey)
|
|
66
|
+
throw new Error('Primary key columns are already unique');
|
|
67
|
+
return new ColumnBuilder({ ...this.props, unique: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const column = (props) => new ColumnBuilder(props);
|
|
71
|
+
exports.column = column;
|
|
72
|
+
const types = {
|
|
73
|
+
bigint: () => column({ type: 'BIGINT', schema: std.bigint() }),
|
|
74
|
+
binary: () => column({ type: 'BINARY', schema: std.instanceOf(Uint8Array) }),
|
|
75
|
+
boolean: () => column({ type: 'BOOLEAN', schema: std.boolean() }),
|
|
76
|
+
// case-insensitive text; postgres needs `CREATE EXTENSION citext` for it
|
|
77
|
+
citext: () => column({ type: 'CITEXT', schema: std.string() }),
|
|
78
|
+
date: () => column({ type: 'DATE', schema: std.isoDate() }),
|
|
79
|
+
datetime: () => column({ type: 'DATETIME', schema: std.date() }),
|
|
80
|
+
// `precision` and `scale` only shape postgres' NUMERIC; values are still
|
|
81
|
+
// numbers, so they're as exact as a double
|
|
82
|
+
decimal: (options) => column({ type: 'DECIMAL', schema: std.number(), ...options }),
|
|
83
|
+
enum: (values) => column({
|
|
84
|
+
type: 'STRING',
|
|
85
|
+
schema: std.enumOf(values),
|
|
86
|
+
length: Math.max(...values.map(value => value.length)),
|
|
87
|
+
}),
|
|
88
|
+
float: () => column({ type: 'FLOAT', schema: std.number() }),
|
|
89
|
+
integer: () => column({ type: 'INTEGER', schema: std.integer() }),
|
|
90
|
+
json: (schema) => column({ type: 'JSON', schema }),
|
|
91
|
+
sequence: () => column({ type: 'SEQUENCE', schema: std.integer(), generated: true }),
|
|
92
|
+
string: ({ size = 255 } = {}) => column({ type: 'STRING', schema: std.string({ max: size }), length: size }),
|
|
93
|
+
text: () => column({ type: 'TEXT', schema: std.string() }),
|
|
94
|
+
uuid: () => column({ type: 'UUID', schema: std.uuid() }),
|
|
95
|
+
};
|
|
96
|
+
exports.t = types;
|
|
97
|
+
const defineTable = (name, shapeFn) => {
|
|
98
|
+
const columns = Object.fromEntries(Object
|
|
99
|
+
.entries(shapeFn(types))
|
|
100
|
+
.map(([key, { props }]) => [(0, utils_1.camelCase)(key), { ...props, key: (0, utils_1.camelCase)(key), name: (0, utils_1.snakeCase)((0, utils_1.camelCase)(key)) }]));
|
|
101
|
+
const aliased = (alias) => ({ name, alias, columns, as: aliased });
|
|
102
|
+
return aliased(name);
|
|
103
|
+
};
|
|
104
|
+
exports.table = defineTable;
|
|
105
|
+
const columnOf = (table, key) => {
|
|
106
|
+
const col = table.columns[key];
|
|
107
|
+
if (!col)
|
|
108
|
+
throw new Error(`Unknown column "${key}" in table "${table.name}"`);
|
|
109
|
+
return col;
|
|
110
|
+
};
|
|
111
|
+
const quote = (ident) => `"${ident.replaceAll('"', '""')}"`;
|
|
112
|
+
const ref = (col, qualifier) => qualifier
|
|
113
|
+
? `${quote(qualifier)}.${quote(col.name)}`
|
|
114
|
+
: quote(col.name);
|
|
115
|
+
class SqlBuilder {
|
|
116
|
+
dialect;
|
|
117
|
+
frags = [];
|
|
118
|
+
params = [];
|
|
119
|
+
constructor(dialect) {
|
|
120
|
+
this.dialect = dialect;
|
|
121
|
+
}
|
|
122
|
+
push(...frags) {
|
|
123
|
+
this.frags.push(...frags);
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
param(value) {
|
|
127
|
+
this.params.push(value);
|
|
128
|
+
return this.push(this.dialect.placeholder(this.params.length));
|
|
129
|
+
}
|
|
130
|
+
join(items, separator, fn) {
|
|
131
|
+
items.forEach((item, i) => {
|
|
132
|
+
if (i > 0)
|
|
133
|
+
this.push(separator);
|
|
134
|
+
fn(item);
|
|
135
|
+
});
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
build() {
|
|
139
|
+
return { sql: this.frags.join(''), params: this.params };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const encode = (dialect, col, value) => value === null || value === undefined
|
|
143
|
+
? null
|
|
144
|
+
: dialect.codecs[col.type].encode(value);
|
|
145
|
+
const decode = (dialect, col, value) => value === null || value === undefined
|
|
146
|
+
? null
|
|
147
|
+
: dialect.codecs[col.type].decode(value);
|
|
148
|
+
const decoder = (dialect, cols) => (row) => Object.fromEntries(cols.map(col => [col.key, decode(dialect, col, row[col.key])]));
|
|
149
|
+
const run = async (db, { sql, params }) => {
|
|
150
|
+
db.loggers.forEach(logger => logger.debug(sql, params));
|
|
151
|
+
try {
|
|
152
|
+
return await db.execute(sql, params);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
db.loggers.forEach(logger => logger.error(sql, params, error));
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
// runs a raw statement, e.g. `CREATE EXTENSION` in a migration
|
|
160
|
+
const exec = async (db, sql, params = []) => {
|
|
161
|
+
await run(db, { sql, params });
|
|
162
|
+
};
|
|
163
|
+
exports.exec = exec;
|
|
164
|
+
const runReturning = async (db, b, table, returning) => {
|
|
165
|
+
if (returning === false) {
|
|
166
|
+
await run(db, b.build());
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const cols = returning === true
|
|
170
|
+
? Object.values(table.columns)
|
|
171
|
+
: returning.map(key => columnOf(table, key));
|
|
172
|
+
// an empty RETURNING isn't valid SQL, but it still takes one to know how
|
|
173
|
+
// many rows were affected
|
|
174
|
+
if (cols.length === 0) {
|
|
175
|
+
const rows = await run(db, b.push(' RETURNING 1').build());
|
|
176
|
+
return rows.map(() => ({}));
|
|
177
|
+
}
|
|
178
|
+
b.push(' RETURNING ').join(cols, ', ', col => b.push(ref(col), ' AS ', quote(col.key)));
|
|
179
|
+
const rows = await run(db, b.build());
|
|
180
|
+
return rows.map(decoder(db.dialect, cols));
|
|
181
|
+
};
|
|
182
|
+
const expr = Object.assign((key, op, value) => ({ key, op, value }), {
|
|
183
|
+
and: (exprs) => ({ group: 'AND', exprs }),
|
|
184
|
+
or: (exprs) => ({ group: 'OR', exprs }),
|
|
185
|
+
});
|
|
186
|
+
exports.expr = expr;
|
|
187
|
+
const toCondition = (table, node) => 'group' in node
|
|
188
|
+
? { group: node.group, conditions: node.exprs.map(expr => toCondition(table, expr)) }
|
|
189
|
+
: { col: columnOf(table, node.key), op: node.op, value: node.value };
|
|
190
|
+
const renderCondition = (b, qualifier) => (condition) => {
|
|
191
|
+
if ('group' in condition) {
|
|
192
|
+
const { group, conditions } = condition;
|
|
193
|
+
if (conditions.length === 0)
|
|
194
|
+
return b.push(group === 'AND' ? '1 = 1' : '1 = 0');
|
|
195
|
+
return b.push('(').join(conditions, ` ${group} `, renderCondition(b, qualifier)).push(')');
|
|
196
|
+
}
|
|
197
|
+
const { col, op, value } = condition;
|
|
198
|
+
if (op === 'is' || op === 'is not')
|
|
199
|
+
return b.push(ref(col, qualifier), ' ', op.toUpperCase(), ' NULL');
|
|
200
|
+
if (op !== 'in' && op !== 'not in')
|
|
201
|
+
return b.push(ref(col, qualifier), ' ', op.toUpperCase(), ' ').param(encode(b.dialect, col, value));
|
|
202
|
+
const values = value;
|
|
203
|
+
if (values.length === 0)
|
|
204
|
+
return b.push(op === 'in' ? '1 = 0' : '1 = 1');
|
|
205
|
+
return b
|
|
206
|
+
.push(ref(col, qualifier), ' ', op.toUpperCase(), ' (')
|
|
207
|
+
.join(values, ', ', v => b.param(encode(b.dialect, col, v)))
|
|
208
|
+
.push(')');
|
|
209
|
+
};
|
|
210
|
+
const renderWhere = (b, conditions, qualifier) => conditions.length === 0
|
|
211
|
+
? b
|
|
212
|
+
: b.push(' WHERE ').join(conditions, ' AND ', renderCondition(b, qualifier));
|
|
213
|
+
const parseChanges = (table, { set = {}, inc = {}, replace = [] }) => {
|
|
214
|
+
const updatable = Object.values(table.columns).filter(col => !col.generated && !col.primaryKey);
|
|
215
|
+
const numeric = updatable.filter(col => ['BIGINT', 'DECIMAL', 'FLOAT', 'INTEGER'].includes(col.type));
|
|
216
|
+
const sets = std.parseOrThrow(std.strictObject(Object.fromEntries(updatable.map(col => [col.key, std.optional(col.schema)]))), set);
|
|
217
|
+
const incs = std.parseOrThrow(std.strictObject(Object.fromEntries(numeric.map(col => [col.key, std.optional(std.number())]))), inc);
|
|
218
|
+
const replaced = replace.map(key => {
|
|
219
|
+
const col = updatable.find(col => col.key === key);
|
|
220
|
+
if (!col)
|
|
221
|
+
throw new Error(`Cannot replace "${key}": it's not an updatable field of "${table.name}"`);
|
|
222
|
+
return col;
|
|
223
|
+
});
|
|
224
|
+
return [
|
|
225
|
+
...Object.entries(sets).filter(([, value]) => value !== undefined).map(([key, value]) => ({ col: columnOf(table, key), op: 'set', value })),
|
|
226
|
+
...Object.entries(incs).filter(([, value]) => value !== undefined).map(([key, value]) => ({ col: columnOf(table, key), op: 'inc', value })),
|
|
227
|
+
...replaced.map(col => ({ col, op: 'replace' })),
|
|
228
|
+
];
|
|
229
|
+
};
|
|
230
|
+
// on conflict, `qualifier` tells the row's current value apart from EXCLUDED's
|
|
231
|
+
const renderAssignments = (b, assignments, qualifier) => b
|
|
232
|
+
.join(assignments, ', ', ({ col, op, value }) => {
|
|
233
|
+
if (op === 'replace')
|
|
234
|
+
return b.push(ref(col), ' = EXCLUDED.', quote(col.name));
|
|
235
|
+
if (op === 'inc')
|
|
236
|
+
return b.push(ref(col), ' = ', ref(col, qualifier), ' + ').param(value);
|
|
237
|
+
return b.push(ref(col), ' = ').param(encode(b.dialect, col, value));
|
|
238
|
+
});
|
|
239
|
+
const CLIENT_GENERATORS = {
|
|
240
|
+
UUID: () => Bun.randomUUIDv7(),
|
|
241
|
+
};
|
|
242
|
+
const isGeneratedByClient = (dialect, col) => col.generated === true && dialect.generated(col) === undefined;
|
|
243
|
+
const resolveInsertValue = (col, row) => {
|
|
244
|
+
if (col.generated) {
|
|
245
|
+
const generate = CLIENT_GENERATORS[col.type];
|
|
246
|
+
if (!generate)
|
|
247
|
+
throw new Error(`Cannot generate a value for column "${col.name}" client-side`);
|
|
248
|
+
return generate();
|
|
249
|
+
}
|
|
250
|
+
if (row[col.key] !== undefined)
|
|
251
|
+
return row[col.key];
|
|
252
|
+
if ('default' in col)
|
|
253
|
+
return typeof col.default === 'function' ? col.default() : col.default;
|
|
254
|
+
if (col.nullable)
|
|
255
|
+
return null;
|
|
256
|
+
return undefined;
|
|
257
|
+
};
|
|
258
|
+
class InsertBuilder {
|
|
259
|
+
table;
|
|
260
|
+
rows;
|
|
261
|
+
constructor(table, rows = []) {
|
|
262
|
+
this.table = table;
|
|
263
|
+
this.rows = rows;
|
|
264
|
+
}
|
|
265
|
+
values(rows) {
|
|
266
|
+
return new InsertBuilder(this.table, [...this.rows, ...(0, utils_1.wrap)(rows)]);
|
|
267
|
+
}
|
|
268
|
+
async insert(db, { returning = true, conflict } = {}) {
|
|
269
|
+
if (this.rows.length === 0)
|
|
270
|
+
return returning === false ? undefined : [];
|
|
271
|
+
const cols = Object.values(this.table.columns);
|
|
272
|
+
const insertable = cols.filter(col => !col.generated || isGeneratedByClient(db.dialect, col));
|
|
273
|
+
const schema = std.array(std.strictObject(Object.fromEntries(insertable.map(col => [col.key, col.schema]))));
|
|
274
|
+
const rows = std.parseOrThrow(schema, this.rows.map(row => ({
|
|
275
|
+
...row,
|
|
276
|
+
...Object.fromEntries(insertable.map(col => [col.key, resolveInsertValue(col, row)])),
|
|
277
|
+
})));
|
|
278
|
+
const b = new SqlBuilder(db.dialect)
|
|
279
|
+
.push('INSERT INTO ', quote(this.table.name), ' (', insertable.map(col => quote(col.name)).join(', '), ') VALUES ');
|
|
280
|
+
b.join(rows, ', ', row => b
|
|
281
|
+
.push('(')
|
|
282
|
+
.join(insertable, ', ', col => b.param(encode(db.dialect, col, row[col.key])))
|
|
283
|
+
.push(')'));
|
|
284
|
+
if (conflict)
|
|
285
|
+
this.renderConflict(b, conflict);
|
|
286
|
+
return await runReturning(db, b, this.table, returning);
|
|
287
|
+
}
|
|
288
|
+
renderConflict(b, conflict) {
|
|
289
|
+
if (conflict.do === 'throw')
|
|
290
|
+
return b;
|
|
291
|
+
b.push(' ON CONFLICT');
|
|
292
|
+
if (conflict.on.length > 0)
|
|
293
|
+
b.push(' (', conflict.on.map(key => quote(columnOf(this.table, key).name)).join(', '), ')');
|
|
294
|
+
if (conflict.do === 'nothing')
|
|
295
|
+
return b.push(' DO NOTHING');
|
|
296
|
+
const assignments = parseChanges(this.table, conflict.do);
|
|
297
|
+
if (assignments.length === 0)
|
|
298
|
+
throw new Error('Nothing to update on conflict: `set`, `inc` and `replace` are empty');
|
|
299
|
+
return renderAssignments(b.push(' DO UPDATE SET '), assignments, this.table.name);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const into = (table) => new InsertBuilder(table);
|
|
303
|
+
exports.into = into;
|
|
304
|
+
// `S` holds the selected fields, every field unless `select` says otherwise
|
|
305
|
+
class QueryBuilder {
|
|
306
|
+
query;
|
|
307
|
+
constructor(query) {
|
|
308
|
+
this.query = query;
|
|
309
|
+
}
|
|
310
|
+
select(keys) {
|
|
311
|
+
return new QueryBuilder({ ...this.query, select: keys });
|
|
312
|
+
}
|
|
313
|
+
where(...args) {
|
|
314
|
+
const node = args.length === 1 ? args[0] : { key: args[0], op: args[1], value: args[2] };
|
|
315
|
+
return new QueryBuilder({ ...this.query, where: [...this.query.where, toCondition(this.query.table, node)] });
|
|
316
|
+
}
|
|
317
|
+
orderBy(keys) {
|
|
318
|
+
const orderBy = keys.map(key => typeof key === 'string'
|
|
319
|
+
? [columnOf(this.query.table, key), 'ASC']
|
|
320
|
+
: [columnOf(this.query.table, key[0]), key[1]]);
|
|
321
|
+
return new QueryBuilder({ ...this.query, orderBy: [...this.query.orderBy, ...orderBy] });
|
|
322
|
+
}
|
|
323
|
+
limit(n) {
|
|
324
|
+
return new QueryBuilder({ ...this.query, limit: n });
|
|
325
|
+
}
|
|
326
|
+
offset(n) {
|
|
327
|
+
return new QueryBuilder({ ...this.query, offset: n });
|
|
328
|
+
}
|
|
329
|
+
async all(db) {
|
|
330
|
+
const { table, select } = this.query;
|
|
331
|
+
const cols = select
|
|
332
|
+
? select.map(key => columnOf(table, key))
|
|
333
|
+
: Object.values(table.columns);
|
|
334
|
+
const rows = await run(db, this.renderSelect(new SqlBuilder(db.dialect), cols).build());
|
|
335
|
+
return rows.map(decoder(db.dialect, cols));
|
|
336
|
+
}
|
|
337
|
+
async one(db) {
|
|
338
|
+
const [row] = await this.top().all(db);
|
|
339
|
+
return row ?? null;
|
|
340
|
+
}
|
|
341
|
+
async exists(db) {
|
|
342
|
+
const rows = await this.top().select([]).all(db);
|
|
343
|
+
return rows.length > 0;
|
|
344
|
+
}
|
|
345
|
+
// counts the rows `select` would return: as a subquery, limit and offset
|
|
346
|
+
// still apply, and postgres doesn't take ORDER BY next to a bare COUNT(*)
|
|
347
|
+
async count(db) {
|
|
348
|
+
const b = new SqlBuilder(db.dialect).push('SELECT COUNT(*) AS "count" FROM (');
|
|
349
|
+
const [row] = await run(db, this.renderSelect(b, []).push(') AS "q"').build());
|
|
350
|
+
// postgres returns COUNT(*) as a bigint
|
|
351
|
+
return Number(row['count']);
|
|
352
|
+
}
|
|
353
|
+
async update(db, { returning = true, ...changes }) {
|
|
354
|
+
this.assertUnbounded('update');
|
|
355
|
+
const { table, where } = this.query;
|
|
356
|
+
const assignments = parseChanges(table, changes);
|
|
357
|
+
if (assignments.length === 0)
|
|
358
|
+
throw new Error('Nothing to update: both `set` and `inc` are empty');
|
|
359
|
+
const b = new SqlBuilder(db.dialect).push('UPDATE ', quote(table.name), ' SET ');
|
|
360
|
+
renderAssignments(b, assignments);
|
|
361
|
+
renderWhere(b, where);
|
|
362
|
+
return await runReturning(db, b, table, returning);
|
|
363
|
+
}
|
|
364
|
+
async delete(db, { returning = false } = {}) {
|
|
365
|
+
this.assertUnbounded('delete');
|
|
366
|
+
const { table, where } = this.query;
|
|
367
|
+
const b = new SqlBuilder(db.dialect).push('DELETE FROM ', quote(table.name));
|
|
368
|
+
renderWhere(b, where);
|
|
369
|
+
return await runReturning(db, b, table, returning);
|
|
370
|
+
}
|
|
371
|
+
renderSelect(b, cols) {
|
|
372
|
+
const { table, where, orderBy, limit, offset } = this.query;
|
|
373
|
+
b.push('SELECT ');
|
|
374
|
+
// an empty select list isn't valid SQL, but each matching row still needs
|
|
375
|
+
// an (empty) object
|
|
376
|
+
if (cols.length === 0)
|
|
377
|
+
b.push('1');
|
|
378
|
+
else
|
|
379
|
+
b.join(cols, ', ', col => b.push(ref(col, table.alias), ' AS ', quote(col.key)));
|
|
380
|
+
b.push(' FROM ', quote(table.name), ' AS ', quote(table.alias));
|
|
381
|
+
renderWhere(b, where, table.alias);
|
|
382
|
+
if (orderBy.length > 0)
|
|
383
|
+
b.push(' ORDER BY ').join(orderBy, ', ', ([col, dir]) => b.push(ref(col, table.alias), ' ', dir));
|
|
384
|
+
// sqlite requires LIMIT whenever there's an OFFSET, and there's no "no
|
|
385
|
+
// limit" value both sqlite and postgres accept other than a huge number
|
|
386
|
+
if (limit !== undefined || offset !== undefined)
|
|
387
|
+
b.push(' LIMIT ').param(limit ?? Number.MAX_SAFE_INTEGER);
|
|
388
|
+
if (offset !== undefined)
|
|
389
|
+
b.push(' OFFSET ').param(offset);
|
|
390
|
+
return b;
|
|
391
|
+
}
|
|
392
|
+
// keeps a `limit(0)`, which can never match
|
|
393
|
+
top() {
|
|
394
|
+
return this.limit(Math.min(this.query.limit ?? 1, 1));
|
|
395
|
+
}
|
|
396
|
+
// UPDATE/DELETE ... ORDER BY/LIMIT isn't portable, so refuse it rather than
|
|
397
|
+
// silently affecting more rows than the query suggests
|
|
398
|
+
assertUnbounded(operation) {
|
|
399
|
+
const { orderBy, limit, offset } = this.query;
|
|
400
|
+
if (orderBy.length > 0 || limit !== undefined || offset !== undefined) {
|
|
401
|
+
throw new Error(`Cannot ${operation} with orderBy, limit or offset`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const from = (table) => new QueryBuilder({ table, where: [], orderBy: [] });
|
|
406
|
+
exports.from = from;
|
|
407
|
+
// ─── ddl ─────────────────────────────────────────────────────────────────────
|
|
408
|
+
const renderColumnDefinition = (dialect, col) => [
|
|
409
|
+
quote(col.name),
|
|
410
|
+
dialect.type(col),
|
|
411
|
+
col.primaryKey && 'PRIMARY KEY',
|
|
412
|
+
col.generated && dialect.generated(col),
|
|
413
|
+
!col.nullable && 'NOT NULL',
|
|
414
|
+
col.unique && 'UNIQUE',
|
|
415
|
+
].filter(Boolean).join(' ');
|
|
416
|
+
const createIndex = (table, keys, { ifNotExists = false, name, unique = false, } = {}) => ({
|
|
417
|
+
onto: async (db) => {
|
|
418
|
+
const cols = keys.map(key => columnOf(table, key).name);
|
|
419
|
+
const index = name ?? `${table.name}_${cols.join('_')}_index`;
|
|
420
|
+
const sql = `CREATE ${unique ? 'UNIQUE ' : ''}INDEX ${ifNotExists ? 'IF NOT EXISTS ' : ''}${quote(index)} ON ${quote(table.name)} (${cols.map(quote).join(', ')})`;
|
|
421
|
+
await run(db, { sql, params: [] });
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
const create = {
|
|
425
|
+
table: (table, { ifNotExists = false } = {}) => ({
|
|
426
|
+
onto: async (db) => {
|
|
427
|
+
const cols = Object.values(table.columns).map(col => renderColumnDefinition(db.dialect, col));
|
|
428
|
+
const sql = `CREATE TABLE ${ifNotExists ? 'IF NOT EXISTS ' : ''}${quote(table.name)} (${cols.join(', ')})`;
|
|
429
|
+
await run(db, { sql, params: [] });
|
|
430
|
+
},
|
|
431
|
+
}),
|
|
432
|
+
index: createIndex,
|
|
433
|
+
uniqueIndex: (table, keys, options = {}) => createIndex(table, keys, { ...options, unique: true }),
|
|
434
|
+
};
|
|
435
|
+
exports.create = create;
|
|
436
|
+
// ─── migrations ──────────────────────────────────────────────────────────────
|
|
437
|
+
const schemaMigrations = defineTable('schema_migrations', t => ({
|
|
438
|
+
id: t.string().primaryKey(),
|
|
439
|
+
migratedAt: t.datetime(),
|
|
440
|
+
}));
|
|
441
|
+
const defineMigrations = (migrations) => async (db) => {
|
|
442
|
+
await create.table(schemaMigrations, { ifNotExists: true }).onto(db);
|
|
443
|
+
const applied = new Set((await from(schemaMigrations).select(['id']).all(db)).map(row => row.id));
|
|
444
|
+
for (const id of Object.keys(migrations).sort()) {
|
|
445
|
+
if (applied.has(id))
|
|
446
|
+
continue;
|
|
447
|
+
await db.transaction(async (tx) => {
|
|
448
|
+
await migrations[id](tx);
|
|
449
|
+
await into(schemaMigrations).values({ id, migratedAt: new Date() }).insert(tx);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
exports.defineMigrations = defineMigrations;
|