@onreza/sqlx-js 0.1.0 → 0.3.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/dist/src/cache.js CHANGED
@@ -2,9 +2,136 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  export function fingerprint(query) {
5
- const norm = query.replace(/\s+/g, " ").trim();
5
+ const norm = normalizeForFingerprint(query);
6
6
  return createHash("sha256").update(norm).digest("hex").slice(0, 16);
7
7
  }
8
+ function normalizeForFingerprint(query) {
9
+ let out = "";
10
+ let pendingSpace = false;
11
+ let i = 0;
12
+ const emit = (text) => {
13
+ if (pendingSpace && out.length > 0)
14
+ out += " ";
15
+ out += text;
16
+ pendingSpace = false;
17
+ };
18
+ const markSpace = () => {
19
+ if (out.length > 0)
20
+ pendingSpace = true;
21
+ };
22
+ while (i < query.length) {
23
+ const ch = query[i];
24
+ if (/\s/.test(ch)) {
25
+ markSpace();
26
+ i++;
27
+ continue;
28
+ }
29
+ if (ch === "-" && query[i + 1] === "-") {
30
+ i = readLineComment(query, i);
31
+ markSpace();
32
+ continue;
33
+ }
34
+ if (ch === "/" && query[i + 1] === "*") {
35
+ i = readBlockComment(query, i);
36
+ markSpace();
37
+ continue;
38
+ }
39
+ if (ch === "'") {
40
+ const next = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
41
+ emit(query.slice(i, next));
42
+ i = next;
43
+ continue;
44
+ }
45
+ if (ch === "\"") {
46
+ const next = readQuotedIdentifier(query, i);
47
+ emit(query.slice(i, next));
48
+ i = next;
49
+ continue;
50
+ }
51
+ if (ch === "$") {
52
+ const next = readDollarQuoted(query, i);
53
+ if (next !== null) {
54
+ emit(query.slice(i, next));
55
+ i = next;
56
+ continue;
57
+ }
58
+ }
59
+ emit(ch);
60
+ i++;
61
+ }
62
+ return out;
63
+ }
64
+ function readSingleQuoted(query, start, escapeBackslash) {
65
+ let i = start + 1;
66
+ while (i < query.length) {
67
+ const ch = query[i];
68
+ if (escapeBackslash && ch === "\\") {
69
+ i += 2;
70
+ continue;
71
+ }
72
+ if (ch === "'") {
73
+ if (query[i + 1] === "'") {
74
+ i += 2;
75
+ continue;
76
+ }
77
+ return i + 1;
78
+ }
79
+ i++;
80
+ }
81
+ return query.length;
82
+ }
83
+ function readQuotedIdentifier(query, start) {
84
+ let i = start + 1;
85
+ while (i < query.length) {
86
+ if (query[i] === "\"") {
87
+ if (query[i + 1] === "\"") {
88
+ i += 2;
89
+ continue;
90
+ }
91
+ return i + 1;
92
+ }
93
+ i++;
94
+ }
95
+ return query.length;
96
+ }
97
+ function readDollarQuoted(query, start) {
98
+ let tagEnd = start + 1;
99
+ while (tagEnd < query.length && /[A-Za-z0-9_]/.test(query[tagEnd]))
100
+ tagEnd++;
101
+ if (query[tagEnd] !== "$")
102
+ return null;
103
+ const tag = query.slice(start, tagEnd + 1);
104
+ const end = query.indexOf(tag, tagEnd + 1);
105
+ return end === -1 ? query.length : end + tag.length;
106
+ }
107
+ function readLineComment(query, start) {
108
+ const end = query.indexOf("\n", start + 2);
109
+ return end === -1 ? query.length : end + 1;
110
+ }
111
+ function readBlockComment(query, start) {
112
+ let depth = 1;
113
+ let i = start + 2;
114
+ while (i < query.length && depth > 0) {
115
+ if (query[i] === "/" && query[i + 1] === "*") {
116
+ depth++;
117
+ i += 2;
118
+ continue;
119
+ }
120
+ if (query[i] === "*" && query[i + 1] === "/") {
121
+ depth--;
122
+ i += 2;
123
+ continue;
124
+ }
125
+ i++;
126
+ }
127
+ return i;
128
+ }
129
+ function isEscapeStringPrefix(query, quoteIndex) {
130
+ if (quoteIndex === 0 || query[quoteIndex - 1]?.toLowerCase() !== "e")
131
+ return false;
132
+ const beforePrefix = query[quoteIndex - 2];
133
+ return beforePrefix === undefined || !/[A-Za-z0-9_$]/.test(beforePrefix);
134
+ }
8
135
  export function effectiveNullable(c) {
9
136
  if (c.override === "non-null")
10
137
  return false;
@@ -24,8 +24,6 @@ export function emitDts(outPath, entries) {
24
24
  lines.push("");
25
25
  emitModule(lines, "@onreza/sqlx-js", entries);
26
26
  lines.push("");
27
- emitModule(lines, "@onreza/sqlx-js/bun", entries);
28
- lines.push("");
29
27
  lines.push("export {};");
30
28
  writeFileSync(outPath, lines.join("\n") + "\n");
31
29
  }
@@ -33,21 +31,23 @@ function emitModule(lines, moduleName, entries) {
33
31
  lines.push(`declare module ${JSON.stringify(moduleName)} {`);
34
32
  lines.push(" interface KnownQueries {");
35
33
  const inlineSeen = new Set();
36
- const inlineEntries = entries.filter((e) => {
37
- if (e.hasInline === true)
38
- return true;
39
- if (e.hasInline === false)
40
- return false;
41
- return !e.filePaths || e.filePaths.length === 0;
42
- });
43
- for (const e of inlineEntries.slice().sort((a, b) => a.query.localeCompare(b.query))) {
44
- if (inlineSeen.has(e.query))
34
+ const inlinePairs = [];
35
+ for (const e of entries) {
36
+ const emitInline = e.hasInline === true || (e.hasInline !== false && (!e.filePaths || e.filePaths.length === 0));
37
+ if (!emitInline)
45
38
  continue;
46
- inlineSeen.add(e.query);
47
- const { params, row } = entrySignature(e);
48
- if (e.degraded)
49
- lines.push(` /** Nullability inference degraded: ${e.degraded.reason}. All result columns conservatively typed as nullable. */`);
50
- lines.push(` ${JSON.stringify(e.query)}: { params: ${params}; row: ${row} };`);
39
+ const queries = e.inlineQueries && e.inlineQueries.length > 0 ? e.inlineQueries : [e.query];
40
+ for (const query of queries)
41
+ inlinePairs.push({ query, entry: e });
42
+ }
43
+ for (const { query, entry } of inlinePairs.slice().sort((a, b) => a.query.localeCompare(b.query))) {
44
+ if (inlineSeen.has(query))
45
+ continue;
46
+ inlineSeen.add(query);
47
+ const { params, row } = entrySignature(entry);
48
+ if (entry.degraded)
49
+ lines.push(` /** Nullability inference degraded: ${entry.degraded.reason}. All result columns conservatively typed as nullable. */`);
50
+ lines.push(` ${JSON.stringify(query)}: { params: ${params}; row: ${row} };`);
51
51
  }
52
52
  lines.push(" }");
53
53
  lines.push("");
@@ -0,0 +1,5 @@
1
+ export type InitOptions = {
2
+ root: string;
3
+ log?: (msg: string) => void;
4
+ };
5
+ export declare function runInit(opts: InitOptions): void;
@@ -0,0 +1,57 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ const CONFIG_TEMPLATE = `import type { SqlxJsConfig } from "@onreza/sqlx-js";
4
+
5
+ const config: SqlxJsConfig = {
6
+ // Map jsonb columns/params to TypeScript types declared in a .d.ts, e.g.
7
+ // "users.settings": "SqlxJsJson.UserSettings",
8
+ jsonbTypes: {},
9
+ // Map PostgreSQL type names to TypeScript types, e.g.
10
+ // geometry: "GeoJSON.Geometry",
11
+ customTypes: {},
12
+ };
13
+
14
+ export default config;
15
+ `;
16
+ const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
17
+ DATABASE_URL=postgres://user:password@localhost:5432/your_db
18
+ # Managed Postgres with TLS:
19
+ # DATABASE_URL=postgres://user:password@db.example.com:5432/your_db?sslmode=verify-full
20
+ `;
21
+ export function runInit(opts) {
22
+ const log = opts.log ?? console.log;
23
+ const created = [];
24
+ const skipped = [];
25
+ const ensureFile = (rel, content) => {
26
+ const full = join(opts.root, rel);
27
+ if (existsSync(full)) {
28
+ skipped.push(rel);
29
+ return;
30
+ }
31
+ mkdirSync(dirname(full), { recursive: true });
32
+ writeFileSync(full, content);
33
+ created.push(rel);
34
+ };
35
+ const ensureDir = (rel) => {
36
+ const full = join(opts.root, rel);
37
+ if (existsSync(full)) {
38
+ skipped.push(`${rel}/`);
39
+ return;
40
+ }
41
+ mkdirSync(full, { recursive: true });
42
+ created.push(`${rel}/`);
43
+ };
44
+ ensureFile("sqlx-js.config.ts", CONFIG_TEMPLATE);
45
+ ensureDir("migrations");
46
+ ensureFile(".env.example", ENV_TEMPLATE);
47
+ for (const f of created)
48
+ log(`created ${f}`);
49
+ for (const f of skipped)
50
+ log(`exists ${f} (left unchanged)`);
51
+ log("");
52
+ log("Next steps:");
53
+ log(" 1. Set DATABASE_URL (see .env.example).");
54
+ log(" 2. Add a migration: sqlx-js migrate add init");
55
+ log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
56
+ log(" 4. Develop locally: sqlx-js migrate dev");
57
+ }
@@ -3,9 +3,22 @@ export type MigrateOptions = {
3
3
  databaseUrl: string;
4
4
  migrationsDir: string;
5
5
  };
6
+ export type MigrationWorkflowOptions = MigrateOptions & {
7
+ root: string;
8
+ cacheDir: string;
9
+ dtsPath: string;
10
+ prune?: boolean;
11
+ shadowUrl?: string;
12
+ shadowAdminUrl?: string;
13
+ lockKey?: number | bigint;
14
+ lockTimeoutMs?: number;
15
+ };
16
+ export type MigrationStore = {
17
+ table: string;
18
+ };
6
19
  export declare const DEFAULT_MIGRATE_LOCK_KEY = 18750938867203960n;
7
- export declare function ensureTable(c: PgClient): Promise<void>;
8
- export declare function listApplied(c: PgClient): Promise<Map<number, {
20
+ export declare function ensureTable(c: PgClient): Promise<MigrationStore>;
21
+ export declare function listApplied(c: PgClient, store?: MigrationStore): Promise<Map<number, {
9
22
  name: string;
10
23
  hash: string;
11
24
  }>>;
@@ -13,6 +26,11 @@ export type ApplyOutcome = {
13
26
  kind: "applied";
14
27
  version: number;
15
28
  name: string;
29
+ } | {
30
+ kind: "adopted";
31
+ version: number;
32
+ name: string;
33
+ replaced: number;
16
34
  } | {
17
35
  kind: "tampered";
18
36
  version: number;
@@ -25,6 +43,123 @@ export type ApplyOutcome = {
25
43
  name: string;
26
44
  error: string;
27
45
  };
46
+ export type PlanOutcome = {
47
+ kind: "pending";
48
+ version: number;
49
+ name: string;
50
+ } | {
51
+ kind: "adoptable";
52
+ version: number;
53
+ name: string;
54
+ replaced: number;
55
+ } | {
56
+ kind: "tampered";
57
+ version: number;
58
+ name: string;
59
+ applied: string;
60
+ current: string;
61
+ } | {
62
+ kind: "failed";
63
+ version: number;
64
+ name: string;
65
+ error: string;
66
+ };
67
+ export type MigrationPlanItem = {
68
+ kind: "apply";
69
+ version: number;
70
+ name: string;
71
+ } | {
72
+ kind: "adopt";
73
+ version: number;
74
+ name: string;
75
+ replaced: number;
76
+ };
77
+ export type MigrationPlanDiagnostic = Extract<PlanOutcome, {
78
+ kind: "tampered" | "failed";
79
+ }>;
80
+ export type MigrationPlanSnapshot = {
81
+ ok: boolean;
82
+ pending: number;
83
+ adoptable: number;
84
+ tampered: number;
85
+ failed: number;
86
+ steps: MigrationPlanItem[];
87
+ diagnostics: MigrationPlanDiagnostic[];
88
+ };
89
+ export type MigrationInfoStatus = "applied" | "pending" | "adoptable" | "superseded" | "tampered" | "failed";
90
+ export type MigrationInfoItem = {
91
+ version: number;
92
+ name: string;
93
+ status: MigrationInfoStatus;
94
+ detail?: string;
95
+ };
96
+ export type MigrationInfoSnapshot = {
97
+ historyTable: string | null;
98
+ summary: Record<MigrationInfoStatus, number>;
99
+ items: MigrationInfoItem[];
100
+ };
101
+ export type MigrationArchive = {
102
+ name: string;
103
+ path: string;
104
+ files: string[];
105
+ };
106
+ export type MigrationCheckIssue = {
107
+ severity: "error";
108
+ code: "invalid-file-name" | "invalid-version" | "duplicate-version" | "orphan-down" | "invalid-squash-metadata" | "invalid-squash-replacement" | "tampered-squash-replacement";
109
+ message: string;
110
+ file?: string;
111
+ version?: number;
112
+ name?: string;
113
+ };
114
+ export type MigrationCheckReport = {
115
+ ok: boolean;
116
+ migrations: number;
117
+ archives: number;
118
+ issues: MigrationCheckIssue[];
119
+ };
120
+ export type SchemaObjectDiff = {
121
+ added: string[];
122
+ removed: string[];
123
+ changed: string[];
124
+ };
125
+ export type SchemaDiffSummary = {
126
+ relations: SchemaObjectDiff;
127
+ types: SchemaObjectDiff;
128
+ functions: SchemaObjectDiff;
129
+ };
130
+ export type RevertDryRunPhase = "validate" | "begin" | "isolate" | "previous-up" | "snapshot-before" | "target-up" | "down" | "snapshot-after" | "rollback";
131
+ export type RevertDryRunOutcome = {
132
+ kind: "noop";
133
+ } | {
134
+ kind: "no-down";
135
+ version: number;
136
+ name: string;
137
+ } | {
138
+ kind: "passed";
139
+ version: number;
140
+ name: string;
141
+ } | {
142
+ kind: "schema-mismatch";
143
+ version: number;
144
+ name: string;
145
+ diff: SchemaDiffSummary;
146
+ } | {
147
+ kind: "failed";
148
+ version?: number;
149
+ name?: string;
150
+ phase: RevertDryRunPhase;
151
+ error: string;
152
+ };
153
+ export declare function checkMigrationFiles(migrationsDir: string): MigrationCheckReport;
154
+ export declare function planPending(c: PgClient, migrationsDir: string, onEvent?: (e: PlanOutcome) => void): Promise<{
155
+ pending: number;
156
+ adoptable: number;
157
+ tampered: number;
158
+ failed: number;
159
+ steps: MigrationPlanItem[];
160
+ }>;
161
+ export declare function inspectMigrationPlan(c: PgClient, migrationsDir: string): Promise<MigrationPlanSnapshot>;
162
+ export declare function inspectMigrations(c: PgClient, migrationsDir: string): Promise<MigrationInfoSnapshot>;
28
163
  export declare function applyPending(c: PgClient, migrationsDir: string, onEvent?: (e: ApplyOutcome) => void): Promise<{
29
164
  applied: number;
30
165
  tampered: number;
@@ -32,11 +167,53 @@ export declare function applyPending(c: PgClient, migrationsDir: string, onEvent
32
167
  }>;
33
168
  export declare function acquireMigrateLock(c: PgClient, lockKey?: number | bigint, timeoutMs?: number): Promise<void>;
34
169
  export declare function releaseMigrateLock(c: PgClient, lockKey?: number | bigint): Promise<void>;
170
+ export declare function sanitizePgDumpSchema(sql: string): string;
171
+ export declare function listMigrationArchives(migrationsDir: string): MigrationArchive[];
172
+ export declare function restoreMigrationArchive(migrationsDir: string, archiveName: string, opts?: {
173
+ force?: boolean;
174
+ }): {
175
+ archiveName: string;
176
+ restored: string[];
177
+ };
178
+ export declare function createSquashMigration(opts: {
179
+ migrationsDir: string;
180
+ name: string;
181
+ schemaSql: string;
182
+ replace?: boolean;
183
+ }): {
184
+ version: number;
185
+ name: string;
186
+ upPath: string;
187
+ replaced: number;
188
+ archiveDir?: string;
189
+ };
190
+ export declare function dumpSchema(databaseUrl: string, pgDumpPath?: string): string;
191
+ export declare function migrateSquash(opts: {
192
+ databaseUrl?: string;
193
+ migrationsDir: string;
194
+ name: string;
195
+ shadowUrl?: string;
196
+ shadowAdminUrl?: string;
197
+ replace?: boolean;
198
+ pgDumpPath?: string;
199
+ lockKey?: number | bigint;
200
+ lockTimeoutMs?: number;
201
+ }): Promise<void>;
35
202
  export declare function migrateRun(opts: MigrateOptions & {
36
203
  lockKey?: number | bigint;
37
204
  lockTimeoutMs?: number;
205
+ dryRun?: boolean;
206
+ json?: boolean;
207
+ }): Promise<void>;
208
+ export declare function migrateInfo(opts: MigrateOptions & {
209
+ json?: boolean;
38
210
  }): Promise<void>;
39
- export declare function migrateInfo(opts: MigrateOptions): Promise<void>;
211
+ export declare function migrateCheck(opts: {
212
+ migrationsDir: string;
213
+ json?: boolean;
214
+ }): void;
215
+ export declare function migrateDev(opts: MigrationWorkflowOptions): Promise<void>;
216
+ export declare function migrateVerify(opts: MigrationWorkflowOptions): Promise<void>;
40
217
  export type RevertOutcome = {
41
218
  kind: "noop";
42
219
  } | {
@@ -54,10 +231,20 @@ export type RevertOutcome = {
54
231
  error: string;
55
232
  };
56
233
  export declare function revertLast(c: PgClient, migrationsDir: string): Promise<RevertOutcome>;
234
+ export declare function checkLastDownMigration(c: PgClient, migrationsDir: string): Promise<RevertDryRunOutcome>;
57
235
  export declare function migrateRevert(opts: MigrateOptions & {
58
236
  lockKey?: number | bigint;
59
237
  lockTimeoutMs?: number;
238
+ dryRun?: boolean;
239
+ shadowUrl?: string;
240
+ shadowAdminUrl?: string;
241
+ json?: boolean;
60
242
  }): Promise<void>;
243
+ export declare function migrateArchiveList(opts: Pick<MigrateOptions, "migrationsDir">): void;
244
+ export declare function migrateArchiveRestore(opts: Pick<MigrateOptions, "migrationsDir"> & {
245
+ name: string;
246
+ force?: boolean;
247
+ }): void;
61
248
  export declare function migrateAdd(opts: MigrateOptions & {
62
249
  name: string;
63
250
  }): void;