basaltdb 0.1.1 → 0.2.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/ORM.md +87 -0
- package/README.md +40 -0
- package/package.json +7 -4
- package/src/index.d.ts +32 -0
- package/src/index.js +107 -0
package/ORM.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `basaltdb` models — capabilities & gaps
|
|
2
|
+
|
|
3
|
+
`basaltdb` ships a **lightweight ORM** (`db.model(name, schema)` → `Model`). This
|
|
4
|
+
doc is honest about what it does, and how it compares to the connectors/ORMs
|
|
5
|
+
people know: **mongoose** (MongoDB), **mysql2 + Sequelize/Prisma** (MySQL).
|
|
6
|
+
|
|
7
|
+
Guiding principle: the model layer only emits SQL the Basalt engine actually
|
|
8
|
+
supports. We'd rather throw a clear error (or omit a feature) than generate SQL
|
|
9
|
+
that fails at runtime. Everything marked 🔒 is blocked on an **engine** feature,
|
|
10
|
+
not the client (see the [roadmap](https://github.com/basalt-db/basalt/blob/main/ROADMAP.md)).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What the model layer does today ✅
|
|
15
|
+
|
|
16
|
+
- **Define a model** with fields as SQL types or friendly aliases
|
|
17
|
+
(`string`→VARCHAR, `int`→INT, `bool`→BOOLEAN, `date`/`datetime`→DATE/TIMESTAMP,
|
|
18
|
+
`decimal`→DECIMAL, …), a `primaryKey`, and per-field `index`.
|
|
19
|
+
- **`sync()`** — create the table (and secondary indexes) if it doesn't exist;
|
|
20
|
+
`sync({force:true})` drops and recreates.
|
|
21
|
+
- **CRUD**: `insert` (one or many), `find` / `findOne` / `findByPk`, `count`,
|
|
22
|
+
`update`, `delete`.
|
|
23
|
+
- **Where-spec** object → SQL: equality, arrays → `IN` (expanded to an `OR`
|
|
24
|
+
chain the engine supports), and operators `= != <> > >= < <= between` (plus
|
|
25
|
+
`eq/ne/gt/gte/lt/lte/in` aliases). Multiple keys AND-ed.
|
|
26
|
+
- **`find` options**: `select`, `orderBy`, `limit`.
|
|
27
|
+
- **`toCreateSQL()`** to inspect the generated DDL.
|
|
28
|
+
- Values are escaped; identifiers never come from user input.
|
|
29
|
+
- Same API embedded (WASM) or over HTTP.
|
|
30
|
+
|
|
31
|
+
## Feature comparison
|
|
32
|
+
|
|
33
|
+
| Capability | mongoose (Mongo) | Sequelize/Prisma (MySQL) | basaltdb Model |
|
|
34
|
+
|---|:--:|:--:|:--:|
|
|
35
|
+
| Define schema / model | ✅ | ✅ | ✅ |
|
|
36
|
+
| Create table/collection from model | ✅ (implicit) | ✅ `sync`/migrate | ✅ `sync()` |
|
|
37
|
+
| Insert one / many | ✅ | ✅ | ✅ |
|
|
38
|
+
| Find with filters | ✅ rich | ✅ rich | ✅ eq / IN / range / between |
|
|
39
|
+
| Update / delete by filter | ✅ | ✅ | ✅ |
|
|
40
|
+
| Count | ✅ | ✅ | ✅ |
|
|
41
|
+
| Order / limit | ✅ | ✅ | ✅ |
|
|
42
|
+
| **Pagination (offset/cursor)** | ✅ | ✅ | ❌ 🔒 no `OFFSET` in engine |
|
|
43
|
+
| **`LIKE` / text search** | ✅ | ✅ | ❌ 🔒 engine has no `LIKE` (throws a clear error) |
|
|
44
|
+
| **Aggregations (group/having)** | ✅ | ✅ | 🟡 via raw `db.query`, not the model API yet |
|
|
45
|
+
| **Relations / joins / populate / include** | ✅ | ✅ | ❌ use raw `JOIN` via `db.query` |
|
|
46
|
+
| **Validation / defaults / hooks** | ✅ | ✅ | ❌ not yet (client-side, could add) |
|
|
47
|
+
| **Migrations (alter schema)** | n/a | ✅ | ❌ 🔒 no `ALTER TABLE` |
|
|
48
|
+
| **Transactions** | ✅ | ✅ | ❌ 🔒 engine has no transactions |
|
|
49
|
+
| **Prepared statements / bind params** | ✅ | ✅ | ❌ 🔒 engine re-parses each call |
|
|
50
|
+
| **Unique / FK / check constraints** | 🟡 | ✅ | ❌ 🔒 only PRIMARY KEY today |
|
|
51
|
+
| **Upsert (`ON CONFLICT`/`save`)** | ✅ | ✅ | ❌ 🔒 no upsert in engine |
|
|
52
|
+
| **Auto-increment / default id** | ✅ | ✅ | ❌ supply the PK yourself |
|
|
53
|
+
| TypeScript types | 🟡/✅ | ✅ | ✅ (`.d.ts`; not schema-inferred generics yet) |
|
|
54
|
+
|
|
55
|
+
## Gaps we *could* close in the client (no engine change needed)
|
|
56
|
+
|
|
57
|
+
These are pure-client and are the natural next PRs to `basalt-js`:
|
|
58
|
+
|
|
59
|
+
1. **Model-level aggregations** — `User.groupBy(['tier'], { count:true, sum:'amount' })`
|
|
60
|
+
emitting `GROUP BY` (the engine supports it).
|
|
61
|
+
2. **Client-side pagination** — `find(..., { limit, page })` by fetching and
|
|
62
|
+
slicing, until the engine gains `OFFSET`.
|
|
63
|
+
3. **Validation / defaults / lifecycle hooks** (`beforeInsert`, `afterFind`).
|
|
64
|
+
4. **`raw()` join helper** — a typed wrapper over `db.query` for `JOIN`s so
|
|
65
|
+
relations feel first-class even without `populate`.
|
|
66
|
+
5. **Schema-inferred TS generics** — infer row types from the model schema so
|
|
67
|
+
`find()` returns a typed object, like Prisma.
|
|
68
|
+
6. **`upsert()` emulation** — try-insert-then-update (non-atomic; documented).
|
|
69
|
+
7. **`describe()` / diff** — compare a model to the live schema and warn on drift.
|
|
70
|
+
|
|
71
|
+
## Gaps that need the engine first 🔒
|
|
72
|
+
|
|
73
|
+
Tracked in the [roadmap](https://github.com/basalt-db/basalt/blob/main/ROADMAP.md):
|
|
74
|
+
`LIKE`/text search & `OFFSET` (Tier 2.1), **transactions** (Tier 1.3),
|
|
75
|
+
**`ALTER TABLE`/migrations** and **constraints/FKs/upsert** (Tier 2.1), and
|
|
76
|
+
**prepared statements** (Tier 2.1). The highest-leverage single item is the
|
|
77
|
+
**PostgreSQL wire protocol** (Tier 4): with it, mature ORMs (Prisma, Sequelize,
|
|
78
|
+
TypeORM, Drizzle) could target Basalt directly and this bespoke layer becomes
|
|
79
|
+
optional.
|
|
80
|
+
|
|
81
|
+
## When to use what
|
|
82
|
+
|
|
83
|
+
- **Model API** — everyday CRUD on a known schema. Cleanest for app code.
|
|
84
|
+
- **Query builder** (`db.table(...)`) — ad-hoc selects with fluent chaining.
|
|
85
|
+
- **Raw `db.query(sql)`** — anything the above don't cover yet (JOINs, GROUP BY,
|
|
86
|
+
window-less analytics). Always available; the model/builder are conveniences on
|
|
87
|
+
top of it.
|
package/README.md
CHANGED
|
@@ -79,6 +79,46 @@ db.table('users').where('id', 6).delete();
|
|
|
79
79
|
Values are escaped (`lit()`); **identifiers are never taken from user input**.
|
|
80
80
|
Call `.toSQL()` on any builder to see the generated statement.
|
|
81
81
|
|
|
82
|
+
## Models (lightweight ORM)
|
|
83
|
+
|
|
84
|
+
Define a table as a model and do typed CRUD without hand-writing SQL. All model
|
|
85
|
+
methods return Promises and work the same embedded or over HTTP.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { Basalt } from 'basaltdb';
|
|
89
|
+
const db = await Basalt.create();
|
|
90
|
+
|
|
91
|
+
const User = db.model('users', {
|
|
92
|
+
id: { type: 'BIGINT', primaryKey: true },
|
|
93
|
+
name: 'string', // string → VARCHAR
|
|
94
|
+
tier: { type: 'int', index: true }, // creates a secondary index on sync()
|
|
95
|
+
active: 'boolean',
|
|
96
|
+
joined: 'date',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await User.sync(); // CREATE TABLE (+ index) if absent
|
|
100
|
+
await User.insert([{ id: 1, name: 'alice', tier: 1, active: true, joined: '2024-01-02' },
|
|
101
|
+
{ id: 2, name: 'bob', tier: 2, active: false }]);
|
|
102
|
+
|
|
103
|
+
await User.find({ tier: 2 }, { orderBy: ['id', 'DESC'], limit: 10 });
|
|
104
|
+
await User.find({ tier: [1, 2] }); // array → IN (expanded to OR)
|
|
105
|
+
await User.find({ id: { '>=': 1, '<': 100 } }); // operators
|
|
106
|
+
await User.findByPk(1);
|
|
107
|
+
await User.count({ active: true });
|
|
108
|
+
await User.update({ tier: 3 }, { id: 2 });
|
|
109
|
+
await User.delete({ id: 2 });
|
|
110
|
+
User.toCreateSQL(); // inspect the DDL
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Where-spec:** `{ col: value }` (equality), `{ col: [a, b] }` (IN), or
|
|
114
|
+
`{ col: { op: value } }` where `op` ∈ `= != <> > >= < <= eq ne gt gte lt lte in between`.
|
|
115
|
+
Multiple keys are AND-ed. Types accept SQL names (`BIGINT`, `VARCHAR`, `DECIMAL`,
|
|
116
|
+
`TIMESTAMP`, …) or friendly aliases (`string`, `int`, `bool`, `date`, `datetime`).
|
|
117
|
+
|
|
118
|
+
See **[ORM.md](ORM.md)** for what the model layer does and does **not** do
|
|
119
|
+
compared to mongoose / Sequelize / Prisma, and why (it's honest about the gaps
|
|
120
|
+
that come from the young engine).
|
|
121
|
+
|
|
82
122
|
## What Basalt supports (and doesn't, yet)
|
|
83
123
|
|
|
84
124
|
This is a young, learning-oriented engine — the builder deliberately stays close
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basaltdb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Official JavaScript/TypeScript client for Basalt — a from-scratch HTAP database engine. Run it embedded in-process via WebAssembly (Node & browser), or talk to a Basalt server over HTTP.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Official JavaScript/TypeScript client & lightweight ORM for Basalt — a from-scratch HTAP database engine. Run it embedded in-process via WebAssembly (Node & browser), or talk to a Basalt server over HTTP. Define models and do typed CRUD.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
"src/index.js",
|
|
17
17
|
"src/index.d.ts",
|
|
18
18
|
"src/engine.js",
|
|
19
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"ORM.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
|
-
"test": "node test/smoke.mjs",
|
|
23
|
+
"test": "node test/smoke.mjs && node test/orm.mjs",
|
|
23
24
|
"prepublishOnly": "npm test"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"database",
|
|
28
|
+
"orm",
|
|
29
|
+
"models",
|
|
27
30
|
"sql",
|
|
28
31
|
"htap",
|
|
29
32
|
"olap",
|
package/src/index.d.ts
CHANGED
|
@@ -36,6 +36,36 @@ export class QueryBuilder {
|
|
|
36
36
|
delete(): Promise<Result> | Result;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export type FieldType =
|
|
40
|
+
| 'BIGINT' | 'INT' | 'SMALLINT' | 'DOUBLE' | 'VARCHAR' | 'CHAR' | 'TEXT'
|
|
41
|
+
| 'BOOLEAN' | 'DATE' | 'TIMESTAMP' | 'DECIMAL'
|
|
42
|
+
| 'string' | 'number' | 'int' | 'integer' | 'bigint' | 'float' | 'double'
|
|
43
|
+
| 'bool' | 'boolean' | 'date' | 'datetime' | 'timestamp' | 'decimal' | (string & {});
|
|
44
|
+
export interface FieldDef { type: FieldType; primaryKey?: boolean; pk?: boolean; index?: boolean; }
|
|
45
|
+
export type ModelSchema = Record<string, FieldType | FieldDef>;
|
|
46
|
+
|
|
47
|
+
/** Operators usable in a where-spec: { age: { '>=': 18 }, id: { in: [1,2] } }. */
|
|
48
|
+
export type WhereSpec = Record<string,
|
|
49
|
+
Value | Value[] | Partial<Record<'='|'!='|'<>'|'>'|'>='|'<'|'<='|'eq'|'ne'|'gt'|'gte'|'lt'|'lte'|'in'|'between'|'like', any>>>;
|
|
50
|
+
export interface FindOptions { select?: string[]; orderBy?: string | [string, 'ASC'|'DESC'|'asc'|'desc']; limit?: number; }
|
|
51
|
+
|
|
52
|
+
/** A table mapped as a model — typed CRUD without hand-writing SQL. */
|
|
53
|
+
export class Model {
|
|
54
|
+
name: string;
|
|
55
|
+
pk: string | null;
|
|
56
|
+
toCreateSQL(): string;
|
|
57
|
+
sync(opts?: { force?: boolean }): Promise<Model>;
|
|
58
|
+
insert(rows: Record<string, Value> | Record<string, Value>[]): Promise<Result>;
|
|
59
|
+
find(where?: WhereSpec, opts?: FindOptions): Promise<Record<string, any>[]>;
|
|
60
|
+
findOne(where?: WhereSpec, opts?: FindOptions): Promise<Record<string, any> | null>;
|
|
61
|
+
findByPk(value: Value): Promise<Record<string, any> | null>;
|
|
62
|
+
count(where?: WhereSpec): Promise<number>;
|
|
63
|
+
update(patch: Record<string, Value>, where?: WhereSpec): Promise<Result>;
|
|
64
|
+
delete(where?: WhereSpec): Promise<Result>;
|
|
65
|
+
}
|
|
66
|
+
export function defineModel(runner: Basalt | BasaltClient, name: string, schema: ModelSchema): Model;
|
|
67
|
+
export function buildWhere(where?: WhereSpec): string;
|
|
68
|
+
|
|
39
69
|
/** Embedded, in-process (WebAssembly) database. */
|
|
40
70
|
export class Basalt {
|
|
41
71
|
static create(): Promise<Basalt>;
|
|
@@ -43,6 +73,7 @@ export class Basalt {
|
|
|
43
73
|
query(sql: string): Record<string, any>[];
|
|
44
74
|
schema(): TableInfo[];
|
|
45
75
|
table(name: string): QueryBuilder;
|
|
76
|
+
model(name: string, schema: ModelSchema): Model;
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
export interface BasaltClientOptions {
|
|
@@ -62,6 +93,7 @@ export class BasaltClient {
|
|
|
62
93
|
dropDatabase(name: string): Promise<any>;
|
|
63
94
|
use(db: string): this;
|
|
64
95
|
table(name: string): QueryBuilder;
|
|
96
|
+
model(name: string, schema: ModelSchema): Model;
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
export function lit(v: Value): string;
|
package/src/index.js
CHANGED
|
@@ -99,6 +99,109 @@ class QueryBuilder {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// ---- models / lightweight ORM --------------------------------------------
|
|
103
|
+
// Define a table as a model, then do typed CRUD without hand-writing SQL. Works
|
|
104
|
+
// on both the embedded engine and the HTTP client (every method returns a
|
|
105
|
+
// Promise). See ORM.md for what it does and does not do vs mongoose/Sequelize.
|
|
106
|
+
const TYPE_ALIASES = {
|
|
107
|
+
string:'VARCHAR', text:'VARCHAR', varchar:'VARCHAR', char:'CHAR',
|
|
108
|
+
int:'INT', integer:'INT', smallint:'SMALLINT', number:'DOUBLE',
|
|
109
|
+
bigint:'BIGINT', long:'BIGINT', float:'DOUBLE', double:'DOUBLE', real:'DOUBLE',
|
|
110
|
+
bool:'BOOLEAN', boolean:'BOOLEAN', date:'DATE', datetime:'TIMESTAMP',
|
|
111
|
+
timestamp:'TIMESTAMP', decimal:'DECIMAL', numeric:'DECIMAL',
|
|
112
|
+
};
|
|
113
|
+
function normField(name, def){
|
|
114
|
+
if (typeof def === 'string') def = { type: def };
|
|
115
|
+
const raw = String(def.type || 'VARCHAR');
|
|
116
|
+
const type = TYPE_ALIASES[raw.toLowerCase()] || raw.toUpperCase();
|
|
117
|
+
return { name, type, primaryKey: !!(def.primaryKey || def.pk), index: !!def.index };
|
|
118
|
+
}
|
|
119
|
+
// Build a WHERE clause from a spec object:
|
|
120
|
+
// { id: 1 } → WHERE id = 1
|
|
121
|
+
// { tier: [1,2] } → WHERE tier IN (1, 2)
|
|
122
|
+
// { age: { '>=': 18, '<': 65 } } → WHERE age >= 18 AND age < 65
|
|
123
|
+
// { name: { like: 'a%' } } / { ts: { between: [lo, hi] } } / { id: { in:[…] } }
|
|
124
|
+
const OPS = { eq:'=', ne:'!=', gt:'>', gte:'>=', lt:'<', lte:'<=',
|
|
125
|
+
'=':'=', '!=':'!=', '<>':'<>', '>':'>', '>=':'>=', '<':'<', '<=':'<=' };
|
|
126
|
+
// IN isn't in the engine's grammar yet, so we expand it to an OR-chain the
|
|
127
|
+
// engine does support: col IN (a,b) → (col = a OR col = b).
|
|
128
|
+
function inClause(col, list){
|
|
129
|
+
if (!Array.isArray(list) || !list.length) throw new BasaltError(`empty IN list for "${col}"`);
|
|
130
|
+
return '(' + list.map(x => `${col} = ${lit(x)}`).join(' OR ') + ')';
|
|
131
|
+
}
|
|
132
|
+
export function buildWhere(where){
|
|
133
|
+
if (!where) return '';
|
|
134
|
+
const keys = Object.keys(where); if (!keys.length) return '';
|
|
135
|
+
const parts = keys.map(col => {
|
|
136
|
+
const v = where[col];
|
|
137
|
+
if (Array.isArray(v)) return inClause(col, v);
|
|
138
|
+
if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
|
|
139
|
+
return Object.entries(v).map(([op, val]) => {
|
|
140
|
+
const o = op.toLowerCase();
|
|
141
|
+
if (o === 'in') return inClause(col, val);
|
|
142
|
+
if (o === 'between') return `${col} BETWEEN ${lit(val[0])} AND ${lit(val[1])}`;
|
|
143
|
+
if (o === 'like') throw new BasaltError('LIKE is not supported by the Basalt engine yet — see ROADMAP.md');
|
|
144
|
+
const sql = OPS[o]; if (!sql) throw new BasaltError('unknown operator: ' + op);
|
|
145
|
+
return `${col} ${sql} ${lit(val)}`;
|
|
146
|
+
}).join(' AND ');
|
|
147
|
+
}
|
|
148
|
+
return `${col} = ${lit(v)}`;
|
|
149
|
+
});
|
|
150
|
+
return ' WHERE ' + parts.join(' AND ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class Model {
|
|
154
|
+
constructor(runner, name, schema){
|
|
155
|
+
this.r = runner; this.name = name; this.schema = schema;
|
|
156
|
+
this.fields = Object.entries(schema).map(([k, v]) => normField(k, v));
|
|
157
|
+
this.pk = (this.fields.find(f => f.primaryKey) || {}).name || null;
|
|
158
|
+
}
|
|
159
|
+
/** The `CREATE TABLE` statement this model maps to. */
|
|
160
|
+
toCreateSQL(){
|
|
161
|
+
const cols = this.fields.map(f => ' ' + f.name + ' ' + f.type + (f.primaryKey ? ' PRIMARY KEY' : ''));
|
|
162
|
+
return `CREATE TABLE ${this.name} (\n${cols.join(',\n')}\n)`;
|
|
163
|
+
}
|
|
164
|
+
indexDDL(){ return this.fields.filter(f => f.index && !f.primaryKey)
|
|
165
|
+
.map(f => `CREATE INDEX ${this.name}_${f.name} ON ${this.name} (${f.name})`); }
|
|
166
|
+
_run(sql){ return Promise.resolve(this.r._run(sql)); }
|
|
167
|
+
/** Create the table (+ indexes) if it doesn't exist. `{force:true}` drops first. */
|
|
168
|
+
async sync(opts = {}){
|
|
169
|
+
const tables = await Promise.resolve(this.r.schema());
|
|
170
|
+
const exists = (tables || []).some(t => t.name === this.name);
|
|
171
|
+
if (exists && opts.force) { await this._run('DROP TABLE IF EXISTS ' + this.name); }
|
|
172
|
+
if (!exists || opts.force) {
|
|
173
|
+
await this._run(this.toCreateSQL());
|
|
174
|
+
for (const d of this.indexDDL()) await this._run(d);
|
|
175
|
+
}
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/** Insert one object or an array of objects (values follow the model's column order). */
|
|
179
|
+
async insert(rows){
|
|
180
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
181
|
+
if (!list.length) return { message: 'INSERT 0' };
|
|
182
|
+
const cols = this.fields.map(f => f.name);
|
|
183
|
+
const values = list.map(o => `(${cols.map(c => lit(o[c] === undefined ? null : o[c])).join(', ')})`).join(', ');
|
|
184
|
+
return this._run(`INSERT INTO ${this.name} (${cols.join(', ')}) VALUES ${values}`);
|
|
185
|
+
}
|
|
186
|
+
/** Find rows. opts: { select:[...], orderBy:'col'|['col','DESC'], limit:n }. */
|
|
187
|
+
async find(where, opts = {}){
|
|
188
|
+
let sql = 'SELECT ' + (opts.select ? opts.select.join(', ') : '*') + ' FROM ' + this.name + buildWhere(where);
|
|
189
|
+
if (opts.orderBy){ const [c, d] = Array.isArray(opts.orderBy) ? opts.orderBy : [opts.orderBy, 'ASC'];
|
|
190
|
+
sql += ` ORDER BY ${c} ${String(d).toUpperCase()}`; }
|
|
191
|
+
if (opts.limit != null) sql += ' LIMIT ' + (opts.limit | 0);
|
|
192
|
+
return (await this._run(sql)).toObjects();
|
|
193
|
+
}
|
|
194
|
+
async findOne(where, opts = {}){ return (await this.find(where, { ...opts, limit: 1 }))[0] ?? null; }
|
|
195
|
+
async findByPk(v){ if (!this.pk) throw new BasaltError(`model ${this.name} has no primary key`);
|
|
196
|
+
return this.findOne({ [this.pk]: v }); }
|
|
197
|
+
async count(where){ return Number((await this._run('SELECT COUNT(*) AS n FROM ' + this.name + buildWhere(where))).toObjects()[0].n); }
|
|
198
|
+
async update(patch, where){ const set = Object.keys(patch).map(c => `${c} = ${lit(patch[c])}`).join(', ');
|
|
199
|
+
return this._run(`UPDATE ${this.name} SET ${set}${buildWhere(where)}`); }
|
|
200
|
+
async delete(where){ return this._run(`DELETE FROM ${this.name}${buildWhere(where)}`); }
|
|
201
|
+
}
|
|
202
|
+
/** Bind a model to any runner (Basalt or BasaltClient). */
|
|
203
|
+
export function defineModel(runner, name, schema){ return new Model(runner, name, schema); }
|
|
204
|
+
|
|
102
205
|
// ---- embedded engine (WASM, in-process) ----------------------------------
|
|
103
206
|
export class Basalt {
|
|
104
207
|
constructor(mod) {
|
|
@@ -121,6 +224,8 @@ export class Basalt {
|
|
|
121
224
|
schema() { return JSON.parse(this._schemaFn()).tables; }
|
|
122
225
|
/** Start a query builder on a table. */
|
|
123
226
|
table(name) { return new QueryBuilder(this, name); }
|
|
227
|
+
/** Define a model (see Model) bound to this database. */
|
|
228
|
+
model(name, schema) { return new Model(this, name, schema); }
|
|
124
229
|
}
|
|
125
230
|
|
|
126
231
|
// ---- HTTP client (talks to a running `basalt` server) --------------------
|
|
@@ -155,6 +260,8 @@ export class BasaltClient {
|
|
|
155
260
|
use(db) { this.db = db; return this; }
|
|
156
261
|
/** Start a query builder on a table. */
|
|
157
262
|
table(name) { return new QueryBuilder(this, name); }
|
|
263
|
+
/** Define a model (see Model) bound to this client. */
|
|
264
|
+
model(name, schema) { return new Model(this, name, schema); }
|
|
158
265
|
}
|
|
159
266
|
|
|
160
267
|
export default Basalt;
|