@mandujs/core 0.20.10 → 0.22.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/README.md +2 -1
- package/package.json +28 -3
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +495 -37
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1671 -80
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +19 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/config/validate.ts +1 -0
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/devtools/ai/mcp-connector.ts +18 -16
- package/src/devtools/client/components/mandu-character.tsx +4 -1
- package/src/devtools/client/components/panel/panel-container.tsx +20 -5
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +679 -23
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- package/src/testing/index.ts +247 -189
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4c — Dialect-specific type mapping.
|
|
3
|
+
*
|
|
4
|
+
* Pure, stateless translation from Mandu's abstract `DdlFieldType` to the
|
|
5
|
+
* concrete SQL column type declaration for each supported provider.
|
|
6
|
+
*
|
|
7
|
+
* This module is intentionally tiny and has no dependency on `emit.ts`; it
|
|
8
|
+
* is imported by `emit.ts` and may be consumed independently by Agents
|
|
9
|
+
* D (generator) and F (QA) for table introspection / parity checks.
|
|
10
|
+
*
|
|
11
|
+
* ## Type map (v1 definitive)
|
|
12
|
+
*
|
|
13
|
+
* | Mandu `type` | Postgres | MySQL | SQLite |
|
|
14
|
+
* |---------------------------|-------------------|---------------|---------|
|
|
15
|
+
* | `string` (no maxLength) | `TEXT` | `VARCHAR(255)`| `TEXT` |
|
|
16
|
+
* | `string` (maxLength = N) | `VARCHAR(N)` | `VARCHAR(N)` | `TEXT` |
|
|
17
|
+
* | `number` | `DOUBLE PRECISION`| `DOUBLE` | `REAL` |
|
|
18
|
+
* | `boolean` | `BOOLEAN` | `TINYINT(1)` | `INTEGER`|
|
|
19
|
+
* | `date` | `TIMESTAMPTZ` | `DATETIME(6)` | `TEXT` |
|
|
20
|
+
* | `uuid` | `UUID` | `CHAR(36)` | `TEXT` |
|
|
21
|
+
* | `email` | `VARCHAR(320)` | `VARCHAR(320)`| `TEXT` |
|
|
22
|
+
* | `url` | `VARCHAR(2048)` | `VARCHAR(2048)`| `TEXT` |
|
|
23
|
+
* | `json` / `array` / `object`| `JSONB` | `JSON` | `TEXT` |
|
|
24
|
+
*
|
|
25
|
+
* Rationale for fixed lengths:
|
|
26
|
+
* - `email` 320 chars — RFC 5321 cap (64 local + 1 @ + 255 domain).
|
|
27
|
+
* - `url` 2048 chars — de facto HTTP URL upper bound used by most
|
|
28
|
+
* browsers and CDNs; shorter than many drivers' TEXT truncation.
|
|
29
|
+
* - `string` default on MySQL is `VARCHAR(255)` because MySQL without a
|
|
30
|
+
* length on `VARCHAR` fails to parse; `TEXT` on MySQL disallows indexes
|
|
31
|
+
* without a prefix spec which breaks `indexed: true`.
|
|
32
|
+
*
|
|
33
|
+
* SQLite notes:
|
|
34
|
+
* - SQLite uses dynamic type affinity; our mapping chooses the canonical
|
|
35
|
+
* affinity names (`TEXT`, `REAL`, `INTEGER`) over the richer PG types so
|
|
36
|
+
* `SELECT typeof(col)` returns the expected affinity in tests.
|
|
37
|
+
* - Booleans are `INTEGER` — SQLite has no native boolean; the driver
|
|
38
|
+
* stores `1`/`0`.
|
|
39
|
+
*/
|
|
40
|
+
import type { DdlDefault, DdlFieldDef, DdlFieldType, SqlProvider } from "./types";
|
|
41
|
+
|
|
42
|
+
// =====================================================================
|
|
43
|
+
// Constants — exposed for testability and to document magic numbers.
|
|
44
|
+
// =====================================================================
|
|
45
|
+
|
|
46
|
+
/** Default VARCHAR length for MySQL when the `string` field omits `maxLength`. */
|
|
47
|
+
export const MYSQL_DEFAULT_STRING_LENGTH = 255;
|
|
48
|
+
|
|
49
|
+
/** RFC 5321 email upper bound. Enforced on MySQL/PG as a VARCHAR cap. */
|
|
50
|
+
export const EMAIL_MAX_LENGTH = 320;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pragmatic HTTP URL cap — shorter than most TEXT truncation points
|
|
54
|
+
* (e.g. IE's historical 2083 limit) and long enough for OAuth redirects.
|
|
55
|
+
*/
|
|
56
|
+
export const URL_MAX_LENGTH = 2048;
|
|
57
|
+
|
|
58
|
+
/** Fixed CHAR length for UUIDs on MySQL (36 = canonical hex-with-dashes). */
|
|
59
|
+
export const UUID_CHAR_LENGTH = 36;
|
|
60
|
+
|
|
61
|
+
// =====================================================================
|
|
62
|
+
// Static per-provider map for non-parameterized types.
|
|
63
|
+
// `string` is resolved dynamically because it depends on `maxLength`.
|
|
64
|
+
// =====================================================================
|
|
65
|
+
|
|
66
|
+
type StaticType = Exclude<DdlFieldType, "string">;
|
|
67
|
+
|
|
68
|
+
const TYPE_MAP: Record<SqlProvider, Record<StaticType, string>> = {
|
|
69
|
+
postgres: {
|
|
70
|
+
number: "DOUBLE PRECISION",
|
|
71
|
+
boolean: "BOOLEAN",
|
|
72
|
+
date: "TIMESTAMPTZ",
|
|
73
|
+
uuid: "UUID",
|
|
74
|
+
email: `VARCHAR(${EMAIL_MAX_LENGTH})`,
|
|
75
|
+
url: `VARCHAR(${URL_MAX_LENGTH})`,
|
|
76
|
+
json: "JSONB",
|
|
77
|
+
array: "JSONB",
|
|
78
|
+
object: "JSONB",
|
|
79
|
+
},
|
|
80
|
+
mysql: {
|
|
81
|
+
number: "DOUBLE",
|
|
82
|
+
boolean: "TINYINT(1)",
|
|
83
|
+
date: "DATETIME(6)",
|
|
84
|
+
uuid: `CHAR(${UUID_CHAR_LENGTH})`,
|
|
85
|
+
email: `VARCHAR(${EMAIL_MAX_LENGTH})`,
|
|
86
|
+
url: `VARCHAR(${URL_MAX_LENGTH})`,
|
|
87
|
+
json: "JSON",
|
|
88
|
+
array: "JSON",
|
|
89
|
+
object: "JSON",
|
|
90
|
+
},
|
|
91
|
+
sqlite: {
|
|
92
|
+
number: "REAL",
|
|
93
|
+
boolean: "INTEGER",
|
|
94
|
+
date: "TEXT",
|
|
95
|
+
uuid: "TEXT",
|
|
96
|
+
email: "TEXT",
|
|
97
|
+
url: "TEXT",
|
|
98
|
+
json: "TEXT",
|
|
99
|
+
array: "TEXT",
|
|
100
|
+
object: "TEXT",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// =====================================================================
|
|
105
|
+
// Public API
|
|
106
|
+
// =====================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Dialect-specific NOW() expression. Used by `DdlDefault.kind === "now"`
|
|
110
|
+
* and by emit.ts when a field's default should map to "current timestamp".
|
|
111
|
+
*
|
|
112
|
+
* - Postgres & MySQL — `NOW()` (ANSI-ish, identical semantics).
|
|
113
|
+
* - SQLite — `CURRENT_TIMESTAMP` (SQLite has no `NOW()`).
|
|
114
|
+
*/
|
|
115
|
+
export function nowExpr(provider: SqlProvider): string {
|
|
116
|
+
switch (provider) {
|
|
117
|
+
case "postgres":
|
|
118
|
+
case "mysql":
|
|
119
|
+
return "NOW()";
|
|
120
|
+
case "sqlite":
|
|
121
|
+
return "CURRENT_TIMESTAMP";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a Mandu field to its dialect-specific SQL column type declaration.
|
|
127
|
+
*
|
|
128
|
+
* `maxLength` applies ONLY to `string` — other types have fixed widths
|
|
129
|
+
* documented in the module header.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* resolveColumnType({ type: "string", maxLength: 100, ... }, "postgres")
|
|
133
|
+
* → "VARCHAR(100)"
|
|
134
|
+
* resolveColumnType({ type: "string", ... }, "postgres")
|
|
135
|
+
* → "TEXT"
|
|
136
|
+
* resolveColumnType({ type: "string", ... }, "mysql")
|
|
137
|
+
* → "VARCHAR(255)"
|
|
138
|
+
*/
|
|
139
|
+
export function resolveColumnType(field: DdlFieldDef, provider: SqlProvider): string {
|
|
140
|
+
if (field.type === "string") {
|
|
141
|
+
return resolveStringType(field.maxLength, provider);
|
|
142
|
+
}
|
|
143
|
+
// Narrowed above — the remaining types are StaticType.
|
|
144
|
+
return TYPE_MAP[provider][field.type];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a `DdlDefault` to the SQL literal that goes after `DEFAULT`.
|
|
149
|
+
*
|
|
150
|
+
* Returns JUST the expression — the caller prepends `DEFAULT ` when
|
|
151
|
+
* composing the column definition.
|
|
152
|
+
*
|
|
153
|
+
* Escaping rules:
|
|
154
|
+
* - `literal: string` — ANSI single-quote escape (`'` → `''`).
|
|
155
|
+
* - `literal: number` — emitted verbatim (already numeric).
|
|
156
|
+
* - `literal: boolean` — Postgres/MySQL `TRUE`/`FALSE`; SQLite `1`/`0`
|
|
157
|
+
* (no native boolean).
|
|
158
|
+
* - `sql: expr` — passed through unchanged. Caller is responsible
|
|
159
|
+
* for cross-dialect portability; this is the escape hatch for things
|
|
160
|
+
* like sequence defaults or Postgres-specific `gen_random_uuid()`.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveDefault(def: NonNullable<DdlFieldDef["default"]>, provider: SqlProvider): string {
|
|
163
|
+
switch (def.kind) {
|
|
164
|
+
case "now":
|
|
165
|
+
return nowExpr(provider);
|
|
166
|
+
case "null":
|
|
167
|
+
return "NULL";
|
|
168
|
+
case "sql":
|
|
169
|
+
return def.expr;
|
|
170
|
+
case "literal":
|
|
171
|
+
return formatLiteral(def.value, provider);
|
|
172
|
+
}
|
|
173
|
+
// Exhaustiveness check — if DdlDefault gains a variant, TS will force this
|
|
174
|
+
// function to be updated before the project type-checks.
|
|
175
|
+
const _exhaustive: never = def;
|
|
176
|
+
return _exhaustive;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// =====================================================================
|
|
180
|
+
// Internals
|
|
181
|
+
// =====================================================================
|
|
182
|
+
|
|
183
|
+
function resolveStringType(maxLength: number | undefined, provider: SqlProvider): string {
|
|
184
|
+
if (provider === "sqlite") return "TEXT"; // SQLite ignores length spec.
|
|
185
|
+
if (typeof maxLength === "number" && maxLength > 0) return `VARCHAR(${maxLength})`;
|
|
186
|
+
if (provider === "postgres") return "TEXT";
|
|
187
|
+
// MySQL default: we MUST pick a length because `VARCHAR` with no length is
|
|
188
|
+
// a parse error. 255 is the convention most ORMs settle on.
|
|
189
|
+
return `VARCHAR(${MYSQL_DEFAULT_STRING_LENGTH})`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatLiteral(value: string | number | boolean, provider: SqlProvider): string {
|
|
193
|
+
if (typeof value === "string") {
|
|
194
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === "number") {
|
|
197
|
+
if (!Number.isFinite(value)) {
|
|
198
|
+
throw new Error(`DDL default literal must be a finite number, got ${String(value)}`);
|
|
199
|
+
}
|
|
200
|
+
return String(value);
|
|
201
|
+
}
|
|
202
|
+
// boolean
|
|
203
|
+
if (provider === "sqlite") return value ? "1" : "0";
|
|
204
|
+
return value ? "TRUE" : "FALSE";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Runtime sanity — exported for test use only.
|
|
208
|
+
export function _internal_allStaticTypes(): StaticType[] {
|
|
209
|
+
return [
|
|
210
|
+
"number",
|
|
211
|
+
"boolean",
|
|
212
|
+
"date",
|
|
213
|
+
"uuid",
|
|
214
|
+
"email",
|
|
215
|
+
"url",
|
|
216
|
+
"json",
|
|
217
|
+
"array",
|
|
218
|
+
"object",
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Re-export DdlDefault for ergonomic consumer imports. */
|
|
223
|
+
export type { DdlDefault };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4c shared types — DDL / diff / migration runtime.
|
|
3
|
+
*
|
|
4
|
+
* This file is the CONTRACT between Agents A (DDL emit), B (diff engine),
|
|
5
|
+
* C (migration runtime), and downstream D (generator) / E (CLI) / F (QA).
|
|
6
|
+
* Do NOT add logic here — pure types only. Logic belongs next to each
|
|
7
|
+
* agent's module.
|
|
8
|
+
*
|
|
9
|
+
* Source of truth for:
|
|
10
|
+
* - SqlProvider, DdlFieldType, DdlDefault, DdlFieldDef, DdlIndex, DdlResource
|
|
11
|
+
* - Snapshot (serialized state of the schema at a point in time)
|
|
12
|
+
* - Change (discriminated union the diff engine emits)
|
|
13
|
+
* - PendingMigration / AppliedMigration (runtime plan + history records)
|
|
14
|
+
* - LockStrategy (per-dialect apply serialization)
|
|
15
|
+
*
|
|
16
|
+
* References:
|
|
17
|
+
* docs/bun/phase-4c-team-plan.md — team plan + I/O contracts
|
|
18
|
+
* docs/rfcs/0001-db-resource-layer.md — design decisions incl. Appendix D
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ========== Provider + field types ==========
|
|
22
|
+
|
|
23
|
+
/** Supported SQL providers. Drizzle/Atlas/sqldef all share this same set. */
|
|
24
|
+
export type SqlProvider = "postgres" | "mysql" | "sqlite";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* DDL-relevant subset of Mandu's existing `FieldType`. v1 supports:
|
|
28
|
+
* string/number/boolean/date/uuid/email/url/json/array/object.
|
|
29
|
+
*
|
|
30
|
+
* `array` and `object` are persisted as JSON columns (JSONB on Postgres,
|
|
31
|
+
* JSON on MySQL, TEXT on SQLite). Users who need typed JSON fields should
|
|
32
|
+
* define a Zod schema in the contract layer; the DB only stores the blob.
|
|
33
|
+
*/
|
|
34
|
+
export type DdlFieldType =
|
|
35
|
+
| "string"
|
|
36
|
+
| "number"
|
|
37
|
+
| "boolean"
|
|
38
|
+
| "date"
|
|
39
|
+
| "uuid"
|
|
40
|
+
| "email"
|
|
41
|
+
| "url"
|
|
42
|
+
| "json"
|
|
43
|
+
| "array"
|
|
44
|
+
| "object";
|
|
45
|
+
|
|
46
|
+
/** How a DEFAULT clause is represented. */
|
|
47
|
+
export type DdlDefault =
|
|
48
|
+
| { kind: "now" } // CURRENT_TIMESTAMP / NOW() — dialect-mapped
|
|
49
|
+
| { kind: "null" }
|
|
50
|
+
| { kind: "literal"; value: string | number | boolean }
|
|
51
|
+
| { kind: "sql"; expr: string }; // raw expression — caller responsible for portability
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* DDL-level field definition. Normalized form derived from
|
|
55
|
+
* ResourceField + ResourceDefinition.persistence.fieldOverrides at
|
|
56
|
+
* snapshot creation time (see `snapshotFromResources`).
|
|
57
|
+
*/
|
|
58
|
+
export interface DdlFieldDef {
|
|
59
|
+
/** Column name in the DB. Derived from Mandu's field key (e.g. `passwordHash` → `password_hash` when `snake_case: true`). */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Abstract Mandu field type. Maps to dialect-specific SQL type via the type map. */
|
|
62
|
+
type: DdlFieldType;
|
|
63
|
+
/** Whether NULL is allowed. Default: false (derived from `ResourceField.required === false`). */
|
|
64
|
+
nullable: boolean;
|
|
65
|
+
/** Primary key flag. Exactly one field per resource should have this set (composite keys are v2+). */
|
|
66
|
+
primary: boolean;
|
|
67
|
+
/** Unique constraint — emits `UNIQUE` on the column (standalone, not composite). */
|
|
68
|
+
unique: boolean;
|
|
69
|
+
/** Whether this field participates in a single-column index (non-unique). */
|
|
70
|
+
indexed: boolean;
|
|
71
|
+
/** DEFAULT clause. */
|
|
72
|
+
default?: DdlDefault;
|
|
73
|
+
/**
|
|
74
|
+
* For `string` type — VARCHAR length hint. Ignored by SQLite (TEXT is
|
|
75
|
+
* unbounded). Postgres prefers TEXT when undefined; MySQL emits
|
|
76
|
+
* VARCHAR(255) default when undefined.
|
|
77
|
+
*/
|
|
78
|
+
maxLength?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Multi-column index definition (composite). Single-column indexes live on `DdlFieldDef.indexed`. */
|
|
82
|
+
export interface DdlIndex {
|
|
83
|
+
name: string; // must be unique within the resource
|
|
84
|
+
fields: string[]; // field names (in order)
|
|
85
|
+
unique: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* DDL-level resource — what actually reaches the emit / diff engines.
|
|
90
|
+
* Produced by `snapshotFromResources` from `ParsedResource[]`. Contains
|
|
91
|
+
* only the information the DB layer cares about.
|
|
92
|
+
*/
|
|
93
|
+
export interface DdlResource {
|
|
94
|
+
/** Table name in the DB. Usually `pluralize(resourceName)` or explicit override. */
|
|
95
|
+
name: string;
|
|
96
|
+
fields: DdlFieldDef[]; // order-preserving; affects emit order
|
|
97
|
+
indexes: DdlIndex[]; // multi-column indexes only; single-column live on fields
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ========== Snapshots ==========
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The full schema state at a point in time. Serialized to JSON and stored
|
|
104
|
+
* at `.mandu/schema/applied.json` after each successful apply.
|
|
105
|
+
* The diff engine compares an old snapshot (or null for first run) to a
|
|
106
|
+
* next snapshot computed from the current resource files.
|
|
107
|
+
*/
|
|
108
|
+
export interface Snapshot {
|
|
109
|
+
/** Format version of this snapshot file. Bump on breaking schema changes. */
|
|
110
|
+
version: 1;
|
|
111
|
+
/** Which provider this snapshot was built for. Diffing across providers is an error. */
|
|
112
|
+
provider: SqlProvider;
|
|
113
|
+
/** Resources in deterministic order (sorted by name). */
|
|
114
|
+
resources: DdlResource[];
|
|
115
|
+
/** When this snapshot was computed. For provenance only — not used by diff. */
|
|
116
|
+
generatedAt: string; // ISO 8601
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ========== Changes ==========
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Discriminated union emitted by the diff engine. Every `Change` is one
|
|
123
|
+
* "atomic" DDL operation. Emit order is deterministic so the generated
|
|
124
|
+
* migration SQL is stable across runs.
|
|
125
|
+
*
|
|
126
|
+
* Rename is NOT auto-detected — the diff engine always emits drop + add.
|
|
127
|
+
* The CLI layer (Agent E) asks the user whether consecutive drop+add are
|
|
128
|
+
* a rename and rewrites the Change list accordingly before SQL emit.
|
|
129
|
+
*/
|
|
130
|
+
export type Change =
|
|
131
|
+
| { kind: "create-table"; resource: DdlResource }
|
|
132
|
+
| { kind: "drop-table"; resourceName: string }
|
|
133
|
+
| { kind: "add-column"; resourceName: string; field: DdlFieldDef }
|
|
134
|
+
| { kind: "drop-column"; resourceName: string; fieldName: string }
|
|
135
|
+
| {
|
|
136
|
+
kind: "alter-column-type";
|
|
137
|
+
resourceName: string;
|
|
138
|
+
fieldName: string;
|
|
139
|
+
fromType: DdlFieldType;
|
|
140
|
+
toType: DdlFieldType;
|
|
141
|
+
/** v1 emits a stub comment. User edits the migration manually. */
|
|
142
|
+
stub: true;
|
|
143
|
+
}
|
|
144
|
+
| { kind: "alter-column-nullable"; resourceName: string; fieldName: string; nullable: boolean }
|
|
145
|
+
| { kind: "alter-column-default"; resourceName: string; fieldName: string; default?: DdlDefault }
|
|
146
|
+
| { kind: "add-index"; resourceName: string; index: DdlIndex }
|
|
147
|
+
| { kind: "drop-index"; resourceName: string; indexName: string }
|
|
148
|
+
| {
|
|
149
|
+
kind: "rename-table";
|
|
150
|
+
oldName: string;
|
|
151
|
+
newName: string;
|
|
152
|
+
/** Emitted only after CLI user confirmation. Diff engine never emits directly. */
|
|
153
|
+
origin: "user-confirmed";
|
|
154
|
+
}
|
|
155
|
+
| {
|
|
156
|
+
kind: "rename-column";
|
|
157
|
+
resourceName: string;
|
|
158
|
+
oldName: string;
|
|
159
|
+
newName: string;
|
|
160
|
+
origin: "user-confirmed";
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// ========== Migration runtime ==========
|
|
164
|
+
|
|
165
|
+
/** A migration file that exists in the migrations directory but has not yet been applied. */
|
|
166
|
+
export interface PendingMigration {
|
|
167
|
+
/** Zero-padded 4-digit sequence, e.g. "0001". Must sort lexicographically. */
|
|
168
|
+
version: string;
|
|
169
|
+
/** Filename relative to the migrations dir, e.g. "0001_create_users.sql". */
|
|
170
|
+
filename: string;
|
|
171
|
+
/** Full SQL text of the migration. */
|
|
172
|
+
sql: string;
|
|
173
|
+
/** SHA-256 of `sql` with `\r\n` normalized to `\n` — used by `__mandu_migrations` for tamper detection. */
|
|
174
|
+
checksum: string;
|
|
175
|
+
/** Filesystem mtime of the migration file. */
|
|
176
|
+
createdAt: Date;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A migration that has been applied — read from the `__mandu_migrations` history table. */
|
|
180
|
+
export interface AppliedMigration {
|
|
181
|
+
version: string;
|
|
182
|
+
filename: string;
|
|
183
|
+
checksum: string;
|
|
184
|
+
appliedAt: Date;
|
|
185
|
+
executionMs: number;
|
|
186
|
+
success: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Snapshot of the migration history at call time. */
|
|
190
|
+
export interface MigrationStatus {
|
|
191
|
+
applied: AppliedMigration[];
|
|
192
|
+
pending: PendingMigration[];
|
|
193
|
+
/** Migrations that exist in history but whose checksum no longer matches the file. */
|
|
194
|
+
tampered: Array<{ version: string; filename: string; storedChecksum: string; currentChecksum: string }>;
|
|
195
|
+
/** Migration files on disk that have no history row and don't match pending (shouldn't happen but guards against dir corruption). */
|
|
196
|
+
orphaned: Array<{ filename: string }>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ========== Lock strategy (per-dialect apply serialization) ==========
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Single-process apply serialization. Multi-instance coordination is out of
|
|
203
|
+
* scope for v1 (RFC §8 non-goals).
|
|
204
|
+
*
|
|
205
|
+
* Defaults per provider (Agent C implements):
|
|
206
|
+
* - postgres → "pg_advisory_lock" (`pg_advisory_lock(bigint)` + `pg_advisory_unlock`)
|
|
207
|
+
* - mysql → "mysql_get_lock" (`GET_LOCK('mandu-migrations', 60)` + `RELEASE_LOCK`)
|
|
208
|
+
* - sqlite → "sqlite_immediate" (`BEGIN IMMEDIATE` for the apply transaction)
|
|
209
|
+
*/
|
|
210
|
+
export type LockStrategy = "pg_advisory_lock" | "mysql_get_lock" | "sqlite_immediate" | "none";
|
|
211
|
+
|
|
212
|
+
// ========== Scope fences — what v1 does NOT cover ==========
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* v1 scope (enforced by Agent A/B — they should NOT handle these):
|
|
216
|
+
* - Foreign keys
|
|
217
|
+
* - CHECK constraints
|
|
218
|
+
* - ENUM types (custom Postgres ENUMs, MySQL ENUM columns)
|
|
219
|
+
* - Computed / GENERATED columns
|
|
220
|
+
* - Partitioning
|
|
221
|
+
* - Triggers, views, stored procedures
|
|
222
|
+
* - Alter column type (stub only — user edits manually)
|
|
223
|
+
* - Rename auto-detection (always drop+add unless CLI prompts user)
|
|
224
|
+
* - Multi-column primary key (composite)
|
|
225
|
+
* - Rollback / DOWN migrations
|
|
226
|
+
* - Repeatable migrations (Flyway `R__` style)
|
|
227
|
+
*
|
|
228
|
+
* Anything outside this list is intentionally out of scope for Phase 4c
|
|
229
|
+
* v1. Expansion lands in 4c.1 / 4c.2 patches after 4c merge.
|
|
230
|
+
*/
|
|
231
|
+
export type Phase4cScopeMarker = typeof _PHASE_4C_V1_SCOPE;
|
|
232
|
+
const _PHASE_4C_V1_SCOPE = Symbol.for("@mandujs/core/resource/ddl/phase-4c-v1");
|