@junejs/juno 0.0.16 → 0.0.18
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 +1 -1
- package/package.json +7 -4
- package/src/batch.ts +13 -10
- package/src/compiler.ts +40 -1
- package/src/index.ts +17 -10
- package/src/types.ts +105 -33
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ backwards. High-signal on purpose — read these, skip re-deriving the obvious.
|
|
|
77
77
|
The framework works fully without Juno. **Tier 1** = bring your own (Prisma/Drizzle),
|
|
78
78
|
untouched. **Tier 2** = your ORM over the ambient `db` resource. **Tier 3** = Juno
|
|
79
79
|
(or any thin shim emitting `recordTableRead`/`recordTableWrite`) for the
|
|
80
|
-
auto-invalidate / auto-batch / live-RSC magic. See `docs/data-layer
|
|
80
|
+
auto-invalidate / auto-batch / live-RSC magic. See `docs/data-layer.md`.
|
|
81
81
|
|
|
82
82
|
For tests or bring-your-own wiring, the explicit handle still works:
|
|
83
83
|
`const j = juno(someDb); await j.table("users").all()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/juno",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Juno — June's ergonomic data layer over the JuneDb contract. The default that ships with the agent-native magic (Tier 3).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@junejs/core": "0.0.
|
|
20
|
-
"@junejs/db": "0.0.
|
|
19
|
+
"@junejs/core": "0.0.18",
|
|
20
|
+
"@junejs/db": "0.0.18"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@junejs/server": "0.0.
|
|
23
|
+
"@junejs/server": "0.0.18",
|
|
24
|
+
"@types/pg": "^8.20.0",
|
|
25
|
+
"mysql2": "^3.22.5",
|
|
26
|
+
"pg": "^8.21.0"
|
|
24
27
|
},
|
|
25
28
|
"repository": {
|
|
26
29
|
"type": "git",
|
package/src/batch.ts
CHANGED
|
@@ -7,8 +7,19 @@
|
|
|
7
7
|
import type { JuneDb } from "@junejs/core/resources";
|
|
8
8
|
import { recordTableRead } from "@junejs/core/instrumentation";
|
|
9
9
|
|
|
10
|
+
import type { SelectNode } from "./ast";
|
|
11
|
+
import { dialectFor } from "./compiler";
|
|
10
12
|
import type { Row } from "./index";
|
|
11
13
|
|
|
14
|
+
// The batched `where <key> in (?, ?, …)` read as an operation node, compiled per
|
|
15
|
+
// batch with the db's dialect (sqlite `?` / Postgres `$n` / MySQL backticks) — so the
|
|
16
|
+
// auto-batch path is multi-dialect like the rest of the Table API. arity = batch size.
|
|
17
|
+
function inQuery<V extends Row>(db: JuneDb, table: string, key: string, keys: unknown[]): Promise<V[]> {
|
|
18
|
+
recordTableRead(table); // batched read still participates in cache auto-tagging
|
|
19
|
+
const node: SelectNode = { kind: "select", from: table, where: [{ col: key, op: "in", arity: keys.length }] };
|
|
20
|
+
return db.query<V>(dialectFor(db).compile(node), keys);
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export type Loader<K, V> = {
|
|
13
24
|
load(key: K): Promise<V | null>;
|
|
14
25
|
};
|
|
@@ -63,11 +74,7 @@ export function tableLoader<V extends Row = Row>(
|
|
|
63
74
|
throw new Error(`unsafe SQL identifier: ${table}.${key}`);
|
|
64
75
|
}
|
|
65
76
|
return createLoader<string | number, V>(
|
|
66
|
-
|
|
67
|
-
recordTableRead(table); // batched read still participates in cache auto-tagging
|
|
68
|
-
const placeholders = keys.map(() => "?").join(", ");
|
|
69
|
-
return db.query<V>(`select * from ${table} where ${key} in (${placeholders})`, keys);
|
|
70
|
-
},
|
|
77
|
+
(keys) => inQuery<V>(db, table, key, keys),
|
|
71
78
|
(row) => (row as Row)[key] as string | number,
|
|
72
79
|
);
|
|
73
80
|
}
|
|
@@ -132,11 +139,7 @@ export function tableListLoader<V extends Row = Row>(
|
|
|
132
139
|
throw new Error(`unsafe SQL identifier: ${table}.${key}`);
|
|
133
140
|
}
|
|
134
141
|
return createGroupLoader<string | number, V>(
|
|
135
|
-
|
|
136
|
-
recordTableRead(table);
|
|
137
|
-
const placeholders = keys.map(() => "?").join(", ");
|
|
138
|
-
return db.query<V>(`select * from ${table} where ${key} in (${placeholders})`, keys);
|
|
139
|
-
},
|
|
142
|
+
(keys) => inQuery<V>(db, table, key, keys),
|
|
140
143
|
(row) => (row as Row)[key] as string | number,
|
|
141
144
|
);
|
|
142
145
|
}
|
package/src/compiler.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// every later call reuses the string — what Kysely-style builders don't do (they
|
|
10
10
|
// recompile per execute). Param VALUES are bound by the caller, never cached.
|
|
11
11
|
|
|
12
|
+
import type { JuneDb, SqlDialect } from "@junejs/core/resources";
|
|
13
|
+
|
|
12
14
|
import type { CmpOp, Node, Predicate } from "./ast";
|
|
13
15
|
|
|
14
16
|
const SYM: Record<CmpOp, string> = {
|
|
@@ -151,7 +153,44 @@ export class PostgresDialect extends Dialect {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
// MySQL: positional `?` placeholders (like sqlite) but backtick-quoted identifiers,
|
|
157
|
+
// and a different upsert — MySQL has no `ON CONFLICT`/`RETURNING`, it keys off any
|
|
158
|
+
// unique/primary index with `ON DUPLICATE KEY UPDATE` and the 8.0.19+ row alias
|
|
159
|
+
// (`AS new … new.col`). So `conflict` columns are ignored (MySQL picks the violated
|
|
160
|
+
// key) and upsert returns no row — the Table layer must re-read if it needs one.
|
|
161
|
+
export class MysqlDialect extends Dialect {
|
|
162
|
+
protected placeholder(): string {
|
|
163
|
+
return "?";
|
|
164
|
+
}
|
|
165
|
+
protected override quoteId(name: string): string {
|
|
166
|
+
return `\`${ident(name)}\``;
|
|
167
|
+
}
|
|
168
|
+
protected override emit(node: Node): string {
|
|
169
|
+
if (node.kind !== "upsert") return super.emit(node);
|
|
170
|
+
const q = (name: string) => this.quoteId(name);
|
|
171
|
+
const cols = node.columns.map(q);
|
|
172
|
+
const vals = node.columns.map(() => this.placeholder()); // `?` is positional — no index
|
|
173
|
+
const set = node.update.map((c) => `${q(c)} = new.${q(c)}`).join(", ");
|
|
174
|
+
return (
|
|
175
|
+
`insert into ${q(node.into)} (${cols.join(", ")}) values (${vals.join(", ")}) as new ` +
|
|
176
|
+
`on duplicate key update ${set}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
// Shared singletons so each dialect's compile-once cache is process-level. `sqlite`
|
|
155
|
-
// (sqlite + D1) is Juno's default; `postgres`
|
|
182
|
+
// (sqlite + D1) is Juno's default; `postgres` / `mysql` compile the same nodes for PG / MySQL.
|
|
156
183
|
export const sqlite = new SqliteDialect();
|
|
157
184
|
export const postgres = new PostgresDialect();
|
|
185
|
+
export const mysql = new MysqlDialect();
|
|
186
|
+
|
|
187
|
+
// The dialect-tag → compiler registry. This is the keystone of multi-dialect runtime:
|
|
188
|
+
// a JuneDb handle declares the SQL it speaks (`db.dialect`), and Juno resolves the
|
|
189
|
+
// matching compiler here — without the framework ever knowing about dialects.
|
|
190
|
+
const BY_DIALECT: Record<SqlDialect, Dialect> = { sqlite, postgres, mysql };
|
|
191
|
+
|
|
192
|
+
// Resolve the compiler for a JuneDb handle. An untagged handle (an older driver, or
|
|
193
|
+
// the contract's optional `dialect`) defaults to sqlite — the safe, edge-first floor.
|
|
194
|
+
export function dialectFor(db: Pick<JuneDb, "dialect">): Dialect {
|
|
195
|
+
return BY_DIALECT[db.dialect ?? "sqlite"];
|
|
196
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Juno — June's ergonomic data layer. A typed table API over the @junejs/core
|
|
2
2
|
// JuneDb contract (so it works over sqlite / D1 / Postgres alike), depending ONLY
|
|
3
|
-
// on @junejs/core (inward; @junejs/core never imports Juno). docs/data-layer
|
|
3
|
+
// on @junejs/core (inward; @junejs/core never imports Juno). docs/data-layer.md.
|
|
4
4
|
//
|
|
5
5
|
// THE MAGIC (Tier 3): every read calls recordTableRead, every write
|
|
6
6
|
// recordTableWrite — @junejs/core's PUBLIC trace contract. That is what makes cache
|
|
@@ -16,13 +16,13 @@ import { db as ambientDb, requestLocal, registerSqlTagger } from "@junejs/db";
|
|
|
16
16
|
|
|
17
17
|
import type { Predicate, SelectNode } from "./ast";
|
|
18
18
|
import { tableLoader, tableListLoader, type Loader, type ListLoader } from "./batch";
|
|
19
|
-
import {
|
|
19
|
+
import { dialectFor, type Dialect } from "./compiler";
|
|
20
20
|
import { taggingDb, tagSql } from "./tag";
|
|
21
21
|
import { emitSchemaTypes } from "./types";
|
|
22
22
|
|
|
23
23
|
export { createLoader, createGroupLoader, tableLoader, tableListLoader, type Loader, type ListLoader } from "./batch";
|
|
24
24
|
export { tablesFromSql, tagSql, taggingDb, type SqlTouch } from "./tag";
|
|
25
|
-
export { Dialect, SqliteDialect, PostgresDialect, sqlite, postgres, ident } from "./compiler";
|
|
25
|
+
export { Dialect, SqliteDialect, PostgresDialect, MysqlDialect, sqlite, postgres, mysql, dialectFor, ident } from "./compiler";
|
|
26
26
|
export { emitSchemaTypes } from "./types";
|
|
27
27
|
export type { Node, SelectNode, InsertNode, UpdateNode, DeleteNode, UpsertNode } from "./ast";
|
|
28
28
|
|
|
@@ -77,6 +77,13 @@ export class Table<T extends Row = Row> {
|
|
|
77
77
|
private readonly loaders: Map<string, unknown> = new Map(),
|
|
78
78
|
) {}
|
|
79
79
|
|
|
80
|
+
// The compiler for THIS table's db — picked from the handle's dialect tag (sqlite
|
|
81
|
+
// by default). So `table()` emits the right SQL whether db is sqlite/D1, Postgres,
|
|
82
|
+
// or MySQL, off the same operation-node AST. Cheap map lookup; called per query.
|
|
83
|
+
private get compiler(): Dialect {
|
|
84
|
+
return dialectFor(this.db);
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
// No `where` → every row. With a `where`, the list of rows matching it (the list
|
|
81
88
|
// counterpart of findBy, same where syntax). A single-column filter ambient-
|
|
82
89
|
// batches like findBy — concurrent `all({user_id})` across components coalesce
|
|
@@ -131,7 +138,7 @@ export class Table<T extends Row = Row> {
|
|
|
131
138
|
};
|
|
132
139
|
if (opts?.limit != null) params.push(opts.limit);
|
|
133
140
|
if (opts?.offset != null) params.push(opts.offset);
|
|
134
|
-
return this.db.query<T>(
|
|
141
|
+
return this.db.query<T>(this.compiler.compile(node), params);
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
async findBy(where: Partial<T>): Promise<T | undefined> {
|
|
@@ -148,7 +155,7 @@ export class Table<T extends Row = Row> {
|
|
|
148
155
|
return (hit ?? undefined) as T | undefined;
|
|
149
156
|
}
|
|
150
157
|
const where2: Predicate[] = keys.map((k) => ({ col: k, op: "eq" }));
|
|
151
|
-
const sql =
|
|
158
|
+
const sql = this.compiler.compile({ kind: "select", from: this.name, where: where2, limit: 1 });
|
|
152
159
|
return this.db.get<T>(sql, Object.values(where));
|
|
153
160
|
}
|
|
154
161
|
|
|
@@ -178,7 +185,7 @@ export class Table<T extends Row = Row> {
|
|
|
178
185
|
|
|
179
186
|
async insert(values: Partial<T>): Promise<RunResult> {
|
|
180
187
|
recordTableWrite(this.name); // auto-invalidate: invokeAction drops table:<name>
|
|
181
|
-
const sql =
|
|
188
|
+
const sql = this.compiler.compile({ kind: "insert", into: this.name, columns: Object.keys(values) });
|
|
182
189
|
return this.db.run(sql, Object.values(values));
|
|
183
190
|
}
|
|
184
191
|
|
|
@@ -194,13 +201,13 @@ export class Table<T extends Row = Row> {
|
|
|
194
201
|
const conflict = Array.isArray(opts.onConflict) ? opts.onConflict : [opts.onConflict];
|
|
195
202
|
const updates = columns.filter((c) => !conflict.includes(c));
|
|
196
203
|
const update = updates.length ? updates : conflict;
|
|
197
|
-
const sql =
|
|
204
|
+
const sql = this.compiler.compile({ kind: "upsert", into: this.name, columns, conflict, update });
|
|
198
205
|
return this.db.get<T>(sql, Object.values(values));
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
async update(where: Partial<T>, values: Partial<T>): Promise<RunResult> {
|
|
202
209
|
recordTableWrite(this.name);
|
|
203
|
-
const sql =
|
|
210
|
+
const sql = this.compiler.compile({
|
|
204
211
|
kind: "update",
|
|
205
212
|
table: this.name,
|
|
206
213
|
set: Object.keys(values),
|
|
@@ -211,7 +218,7 @@ export class Table<T extends Row = Row> {
|
|
|
211
218
|
|
|
212
219
|
async delete(where: Partial<T>): Promise<RunResult> {
|
|
213
220
|
recordTableWrite(this.name);
|
|
214
|
-
const sql =
|
|
221
|
+
const sql = this.compiler.compile({ kind: "delete", from: this.name, where: Object.keys(where) });
|
|
215
222
|
return this.db.run(sql, Object.values(where));
|
|
216
223
|
}
|
|
217
224
|
|
|
@@ -241,7 +248,7 @@ export type Juno = {
|
|
|
241
248
|
writes(...tables: string[]): void;
|
|
242
249
|
};
|
|
243
250
|
|
|
244
|
-
// Wrap any JuneDb handle (
|
|
251
|
+
// Wrap any JuneDb handle (the ambient `db`) in Juno's ergonomic surface. Build ONE per
|
|
245
252
|
// request: the ambient-loader registry it carries is what makes concurrent
|
|
246
253
|
// findBy across components coalesce — and scoping it to the per-request handle
|
|
247
254
|
// is what keeps keys from leaking across requests.
|
package/src/types.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
// Stage 3b — schema codegen. Introspect a migrated
|
|
1
|
+
// Stage 3b / step 4 — schema codegen. Introspect a migrated database and emit the
|
|
2
2
|
// `declare module "@junejs/juno"` augmentation that powers Stage 3a's `table()`
|
|
3
|
-
// inference. `june db types` writes the result to db/schema.d.ts. The schema of
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// inference. `june db types` writes the result to db/schema.d.ts. The schema of record
|
|
4
|
+
// stays in SQL (db/migrations/); this only DERIVES types from it, so the two never
|
|
5
|
+
// drift. Multi-dialect: sqlite/D1 via PRAGMA, Postgres/MySQL via information_schema —
|
|
6
|
+
// dispatched on the JuneDb's `dialect` tag.
|
|
6
7
|
|
|
7
8
|
import type { JuneDb } from "@junejs/core/resources";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
function
|
|
10
|
+
type Field = { name: string; ts: string };
|
|
11
|
+
|
|
12
|
+
// Bare identifier if it's a safe JS property name, else a quoted key.
|
|
13
|
+
function propKey(name: string): string {
|
|
14
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nul = (ts: string, nullable: boolean) => (nullable ? `${ts} | null` : ts);
|
|
18
|
+
|
|
19
|
+
// --- SQLite / D1 — PRAGMA + type affinity -------------------------------------
|
|
20
|
+
type SqliteCol = { name: string; type: string; nn: number; pk: number };
|
|
21
|
+
|
|
22
|
+
function sqliteType(declared: string, notnull: number, pk: number): string {
|
|
13
23
|
const t = declared.toUpperCase();
|
|
14
24
|
let base: string;
|
|
15
25
|
if (t.includes("INT")) base = "number"; // INTEGER affinity
|
|
@@ -17,42 +27,91 @@ function tsType(declared: string, notnull: number, pk: number): string {
|
|
|
17
27
|
else if (t === "" || t.includes("BLOB")) base = "Uint8Array"; // BLOB
|
|
18
28
|
else if (t.includes("REAL") || t.includes("FLOA") || t.includes("DOUB")) base = "number"; // REAL
|
|
19
29
|
else base = "number"; // NUMERIC (decimal / boolean / date stored as 0/1/epoch)
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
return nullable ? `${base} | null` : base;
|
|
30
|
+
// Nullable unless declared NOT NULL or part of the PK (an INTEGER PRIMARY KEY shows
|
|
31
|
+
// notnull=0 but is the never-null rowid alias).
|
|
32
|
+
return nul(base, notnull === 0 && pk === 0);
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type TableRow = { name: string };
|
|
32
|
-
type ColRow = { name: string; type: string; nn: number; pk: number };
|
|
33
|
-
|
|
34
|
-
// Introspect every user table (skipping sqlite internals + June's migration ledger)
|
|
35
|
-
// and return the augmentation file text. Reads only — never mutates the database.
|
|
36
|
-
export async function emitSchemaTypes(db: JuneDb): Promise<string> {
|
|
37
|
-
const tables = await db.query<TableRow>(
|
|
35
|
+
async function introspectSqlite(db: JuneDb): Promise<[string, Field[]][]> {
|
|
36
|
+
const tables = await db.query<{ name: string }>(
|
|
38
37
|
"select name from sqlite_master where type = 'table' " +
|
|
39
38
|
"and name not like 'sqlite_%' and name <> '_june_migrations' order by name",
|
|
40
39
|
);
|
|
40
|
+
const out: [string, Field[]][] = [];
|
|
41
|
+
for (const { name } of tables) {
|
|
42
|
+
// pragma_table_info(?) — table-valued form takes a BOUND param (injection-safe).
|
|
43
|
+
const cols = await db.query<SqliteCol>('select name, type, "notnull" as nn, pk from pragma_table_info(?)', [name]);
|
|
44
|
+
out.push([name, cols.map((c) => ({ name: c.name, ts: sqliteType(c.type, c.nn, c.pk) }))]);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- information_schema (Postgres / MySQL) ------------------------------------
|
|
50
|
+
type InfoCol = { name: string; type: string; nullable: string };
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
// Postgres data_type → TS, matching what node-postgres actually returns: timestamps/
|
|
53
|
+
// dates parse to Date, bigint/numeric come back as string (precision), bytea as bytes.
|
|
54
|
+
function postgresType(dataType: string, nullable: boolean): string {
|
|
55
|
+
const t = dataType.toLowerCase();
|
|
56
|
+
let base: string;
|
|
57
|
+
if (t === "boolean") base = "boolean";
|
|
58
|
+
else if (t === "smallint" || t === "integer" || t === "real" || t === "double precision") base = "number";
|
|
59
|
+
else if (t === "bigint" || t === "numeric" || t === "decimal") base = "string";
|
|
60
|
+
else if (t === "bytea") base = "Uint8Array";
|
|
61
|
+
else if (t === "json" || t === "jsonb") base = "unknown";
|
|
62
|
+
else if (t === "date" || t.startsWith("timestamp")) base = "Date";
|
|
63
|
+
else if (t === "uuid" || t === "text" || t.startsWith("character") || t === "name" || t.startsWith("time")) base = "string";
|
|
64
|
+
else base = "unknown";
|
|
65
|
+
return nul(base, nullable);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MySQL data_type → TS, matching mysql2's defaults: DATE/DATETIME/TIMESTAMP → Date,
|
|
69
|
+
// DECIMAL → string, BLOB/BINARY → bytes, JSON parsed (unknown).
|
|
70
|
+
function mysqlType(dataType: string, nullable: boolean): string {
|
|
71
|
+
const t = dataType.toLowerCase();
|
|
72
|
+
let base: string;
|
|
73
|
+
if (["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "float", "double", "year"].includes(t)) base = "number";
|
|
74
|
+
else if (t === "decimal" || t === "numeric") base = "string";
|
|
75
|
+
else if (["char", "varchar", "text", "tinytext", "mediumtext", "longtext", "enum", "set", "time"].includes(t)) base = "string";
|
|
76
|
+
else if (t === "date" || t === "datetime" || t === "timestamp") base = "Date";
|
|
77
|
+
else if (t === "json") base = "unknown";
|
|
78
|
+
else if (t.includes("blob") || t.includes("binary") || t === "bit") base = "Uint8Array";
|
|
79
|
+
else base = "unknown";
|
|
80
|
+
return nul(base, nullable);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generic information_schema walk; `ph` is the dialect's placeholder, `scope` the SQL
|
|
84
|
+
// expression for the current schema (current_schema() / database()), `map` the type map.
|
|
85
|
+
async function introspectInfoSchema(
|
|
86
|
+
db: JuneDb,
|
|
87
|
+
ph: string,
|
|
88
|
+
scope: string,
|
|
89
|
+
map: (dataType: string, nullable: boolean) => string,
|
|
90
|
+
): Promise<[string, Field[]][]> {
|
|
91
|
+
const tables = await db.query<{ name: string }>(
|
|
92
|
+
`select table_name as name from information_schema.tables ` +
|
|
93
|
+
`where table_schema = ${scope} and table_type = 'BASE TABLE' ` +
|
|
94
|
+
`and table_name <> '_june_migrations' order by table_name`,
|
|
95
|
+
);
|
|
96
|
+
const out: [string, Field[]][] = [];
|
|
43
97
|
for (const { name } of tables) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
const cols = await db.query<InfoCol>(
|
|
99
|
+
`select column_name as name, data_type as type, is_nullable as nullable ` +
|
|
100
|
+
`from information_schema.columns where table_schema = ${scope} and table_name = ${ph} ` +
|
|
101
|
+
`order by ordinal_position`,
|
|
48
102
|
[name],
|
|
49
103
|
);
|
|
50
|
-
|
|
51
|
-
.map((c) => ` ${propKey(c.name)}: ${tsType(c.type, c.nn, c.pk)};`)
|
|
52
|
-
.join("\n");
|
|
53
|
-
ifaces.push(` ${propKey(name)}: {\n${fields}\n };`);
|
|
104
|
+
out.push([name, cols.map((c) => ({ name: c.name, ts: map(c.type, c.nullable === "YES") }))]);
|
|
54
105
|
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
55
108
|
|
|
109
|
+
// --- assembly + dispatch ------------------------------------------------------
|
|
110
|
+
function assemble(tables: [string, Field[]][]): string {
|
|
111
|
+
const ifaces = tables.map(([name, fields]) => {
|
|
112
|
+
const lines = fields.map((f) => ` ${propKey(f.name)}: ${f.ts};`).join("\n");
|
|
113
|
+
return ` ${propKey(name)}: {\n${lines}\n };`;
|
|
114
|
+
});
|
|
56
115
|
return (
|
|
57
116
|
"// Generated by `june db types`. Do not edit — re-run after a migration.\n" +
|
|
58
117
|
'import "@junejs/juno";\n\n' +
|
|
@@ -62,3 +121,16 @@ export async function emitSchemaTypes(db: JuneDb): Promise<string> {
|
|
|
62
121
|
"\n }\n}\n"
|
|
63
122
|
);
|
|
64
123
|
}
|
|
124
|
+
|
|
125
|
+
// Introspect every user table (skipping the migration ledger + engine internals) and
|
|
126
|
+
// emit the augmentation text. Reads only — never mutates the database.
|
|
127
|
+
export async function emitSchemaTypes(db: JuneDb): Promise<string> {
|
|
128
|
+
const dialect = db.dialect ?? "sqlite";
|
|
129
|
+
const tables =
|
|
130
|
+
dialect === "postgres"
|
|
131
|
+
? await introspectInfoSchema(db, "$1", "current_schema()", postgresType)
|
|
132
|
+
: dialect === "mysql"
|
|
133
|
+
? await introspectInfoSchema(db, "?", "database()", mysqlType)
|
|
134
|
+
: await introspectSqlite(db);
|
|
135
|
+
return assemble(tables);
|
|
136
|
+
}
|