zenstack-kit 0.1.4 → 0.1.5
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 +10 -0
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +4 -1
- package/dist/cli/commands.d.ts +1 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +12 -3
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +11 -9
- package/dist/migrations/prisma/apply.d.ts +52 -0
- package/dist/migrations/prisma/apply.d.ts.map +1 -0
- package/dist/migrations/prisma/apply.js +382 -0
- package/dist/migrations/prisma/create.d.ts +63 -0
- package/dist/migrations/prisma/create.d.ts.map +1 -0
- package/dist/migrations/prisma/create.js +119 -0
- package/dist/migrations/prisma/diff.d.ts +104 -0
- package/dist/migrations/prisma/diff.d.ts.map +1 -0
- package/dist/migrations/prisma/diff.js +442 -0
- package/dist/migrations/prisma/log.d.ts +31 -0
- package/dist/migrations/prisma/log.d.ts.map +1 -0
- package/dist/migrations/prisma/log.js +101 -0
- package/dist/migrations/prisma/rename.d.ts +23 -0
- package/dist/migrations/prisma/rename.d.ts.map +1 -0
- package/dist/migrations/prisma/rename.js +57 -0
- package/dist/migrations/prisma/snapshot.d.ts +32 -0
- package/dist/migrations/prisma/snapshot.d.ts.map +1 -0
- package/dist/migrations/prisma/snapshot.js +65 -0
- package/dist/migrations/prisma.d.ts +5 -202
- package/dist/migrations/prisma.d.ts.map +1 -1
- package/dist/migrations/prisma.js +5 -1168
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,7 @@ Options:
|
|
|
124
124
|
- `-m, --migrations <path>` - Migrations directory
|
|
125
125
|
- `--baseline` - Create snapshot only, no migration
|
|
126
126
|
- `--create-initial` - Create snapshot and initial migration
|
|
127
|
+
- `-c, --config <path>` - Path to zenstack-kit config file
|
|
127
128
|
|
|
128
129
|
### `zenstack-kit migrate:generate`
|
|
129
130
|
|
|
@@ -138,6 +139,7 @@ Options:
|
|
|
138
139
|
- `-s, --schema <path>` - Path to ZenStack schema
|
|
139
140
|
- `-m, --migrations <path>` - Migrations directory
|
|
140
141
|
- `--dialect <dialect>` - Database dialect (`sqlite`, `postgres`, `mysql`)
|
|
142
|
+
- `-c, --config <path>` - Path to zenstack-kit config file
|
|
141
143
|
|
|
142
144
|
### `zenstack-kit migrate:apply`
|
|
143
145
|
|
|
@@ -153,6 +155,8 @@ Options:
|
|
|
153
155
|
- `--url <url>` - Database connection URL (overrides config)
|
|
154
156
|
- `--table <name>` - Migrations table name (default: `_prisma_migrations`)
|
|
155
157
|
- `--db-schema <name>` - Database schema for migrations table (PostgreSQL only, default: `public`)
|
|
158
|
+
- `--preview` - Preview pending migrations without applying
|
|
159
|
+
- `-c, --config <path>` - Path to zenstack-kit config file
|
|
156
160
|
|
|
157
161
|
### `zenstack-kit pull`
|
|
158
162
|
|
|
@@ -166,6 +170,7 @@ Options:
|
|
|
166
170
|
- `-o, --output <path>` - Output path for schema (default: `./schema.zmodel`)
|
|
167
171
|
- `--dialect <dialect>` - Database dialect
|
|
168
172
|
- `--url <url>` - Database connection URL
|
|
173
|
+
- `-c, --config <path>` - Path to zenstack-kit config file
|
|
169
174
|
|
|
170
175
|
Features:
|
|
171
176
|
- Detects tables, columns, and types
|
|
@@ -289,6 +294,11 @@ const { db, destroy } = await createKyselyAdapter({
|
|
|
289
294
|
await destroy();
|
|
290
295
|
```
|
|
291
296
|
|
|
297
|
+
## Experimental
|
|
298
|
+
|
|
299
|
+
The `introspectSchema` API is experimental and uses a simplified parser. Expect
|
|
300
|
+
limitations with complex schemas.
|
|
301
|
+
|
|
292
302
|
## Prisma Compatibility
|
|
293
303
|
|
|
294
304
|
zenstack-kit is designed to be compatible with Prisma's migration system:
|
package/dist/cli/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AA0SH,wBAAgB,MAAM,SAkBrB"}
|
package/dist/cli/app.js
CHANGED
|
@@ -77,6 +77,9 @@ function parseArgs() {
|
|
|
77
77
|
else if (arg === "--force" || arg === "-f") {
|
|
78
78
|
options.force = true;
|
|
79
79
|
}
|
|
80
|
+
else if (arg === "--config" || arg === "-c") {
|
|
81
|
+
options.config = args[++i];
|
|
82
|
+
}
|
|
80
83
|
}
|
|
81
84
|
return { command, options };
|
|
82
85
|
}
|
|
@@ -98,7 +101,7 @@ function Status({ type, message }) {
|
|
|
98
101
|
}
|
|
99
102
|
// Help display component
|
|
100
103
|
function HelpDisplay() {
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "zenstack-kit" }), _jsx(Text, { dimColor: true, children: "Database tooling for ZenStack schemas" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Commands:" }), commands.filter(c => c.value !== "exit").map((cmd) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Box, { width: 20, children: _jsx(Text, { color: "yellow", children: cmd.label }) }), _jsx(Text, { dimColor: true, children: cmd.description })] }, cmd.value))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Options:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "-s, --schema <path> Path to ZenStack schema" }), _jsx(Text, { dimColor: true, children: "-m, --migrations <path> Migrations directory" }), _jsx(Text, { dimColor: true, children: "-n, --name <name> Migration name" }), _jsx(Text, { dimColor: true, children: "--dialect <dialect> Database dialect (sqlite, postgres, mysql)" }), _jsx(Text, { dimColor: true, children: "--url <url> Database connection URL" }), _jsx(Text, { dimColor: true, children: "--create-initial Create initial migration (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--baseline Create baseline only (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--preview Preview pending migrations without applying" }), _jsx(Text, { dimColor: true, children: "-f, --force Force operation without confirmation" })] })] }));
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "zenstack-kit" }), _jsx(Text, { dimColor: true, children: "Database tooling for ZenStack schemas" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Commands:" }), commands.filter(c => c.value !== "exit").map((cmd) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Box, { width: 20, children: _jsx(Text, { color: "yellow", children: cmd.label }) }), _jsx(Text, { dimColor: true, children: cmd.description })] }, cmd.value))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Options:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "-s, --schema <path> Path to ZenStack schema" }), _jsx(Text, { dimColor: true, children: "-m, --migrations <path> Migrations directory" }), _jsx(Text, { dimColor: true, children: "-n, --name <name> Migration name" }), _jsx(Text, { dimColor: true, children: "--dialect <dialect> Database dialect (sqlite, postgres, mysql)" }), _jsx(Text, { dimColor: true, children: "--url <url> Database connection URL" }), _jsx(Text, { dimColor: true, children: "--create-initial Create initial migration (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--baseline Create baseline only (skip prompt)" }), _jsx(Text, { dimColor: true, children: "--preview Preview pending migrations without applying" }), _jsx(Text, { dimColor: true, children: "-f, --force Force operation without confirmation" }), _jsx(Text, { dimColor: true, children: "-c, --config <path> Path to zenstack-kit config file" })] })] }));
|
|
102
105
|
}
|
|
103
106
|
function CliApp({ initialCommand, options }) {
|
|
104
107
|
const { exit } = useApp();
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAE9F,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,cAAc,CAAC;IACxB,GAAG,EAAE,KAAK,CAAC;IACX,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IAC9D,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,UAAU,GAAG,gBAAgB,CAAC,CAAC;IAC/D,iBAAiB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxF,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,sBAAsB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACrF;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CAC1C,CAAC,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/commands.js
CHANGED
|
@@ -19,8 +19,11 @@ export class CommandError extends Error {
|
|
|
19
19
|
* Load and validate config, returning resolved paths
|
|
20
20
|
*/
|
|
21
21
|
export async function resolveConfig(ctx) {
|
|
22
|
-
const loaded = await loadConfig(ctx.cwd);
|
|
22
|
+
const loaded = await loadConfig(ctx.cwd, ctx.options.config);
|
|
23
23
|
if (!loaded) {
|
|
24
|
+
if (ctx.options.config) {
|
|
25
|
+
throw new CommandError(`Config file not found: ${ctx.options.config}`);
|
|
26
|
+
}
|
|
24
27
|
throw new CommandError("No zenstack-kit config file found.");
|
|
25
28
|
}
|
|
26
29
|
const { config, configDir } = loaded;
|
|
@@ -121,6 +124,7 @@ export async function runMigrateGenerate(ctx) {
|
|
|
121
124
|
}
|
|
122
125
|
ctx.log("success", `Migration created: ${migration.folderName}/migration.sql`);
|
|
123
126
|
ctx.log("info", `Path: ${migration.folderPath}`);
|
|
127
|
+
ctx.log("info", "Next: run 'zenstack-kit migrate apply' (or --preview to review SQL).");
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
130
|
* migrate:apply command
|
|
@@ -146,7 +150,7 @@ export async function runMigrateApply(ctx) {
|
|
|
146
150
|
const databasePath = dialect === "sqlite" ? connectionUrl : undefined;
|
|
147
151
|
// Preview mode - show pending migrations without applying
|
|
148
152
|
if (ctx.options.preview) {
|
|
149
|
-
ctx.log("info", "Preview mode -
|
|
153
|
+
ctx.log("info", "Preview mode - no changes will be applied.");
|
|
150
154
|
const preview = await previewPrismaMigrations({
|
|
151
155
|
migrationsFolder: outputPath,
|
|
152
156
|
dialect,
|
|
@@ -162,8 +166,12 @@ export async function runMigrateApply(ctx) {
|
|
|
162
166
|
}
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
169
|
+
ctx.log("info", `Pending migrations: ${preview.pending.length}`);
|
|
170
|
+
if (preview.alreadyApplied.length > 0) {
|
|
171
|
+
ctx.log("info", `${preview.alreadyApplied.length} migration(s) already applied`);
|
|
172
|
+
}
|
|
165
173
|
for (const migration of preview.pending) {
|
|
166
|
-
ctx.log("info", `
|
|
174
|
+
ctx.log("info", `Migration: ${migration.name}`);
|
|
167
175
|
ctx.log("info", `SQL:\n${migration.sql}`);
|
|
168
176
|
}
|
|
169
177
|
return;
|
|
@@ -335,6 +343,7 @@ export async function runPull(ctx) {
|
|
|
335
343
|
});
|
|
336
344
|
ctx.log("success", `Schema generated: ${result.outputPath}`);
|
|
337
345
|
ctx.log("info", `${result.tableCount} table(s) introspected`);
|
|
346
|
+
ctx.log("info", "Next: review the schema, then run 'zenstack-kit init' to reset the snapshot.");
|
|
338
347
|
// If we have existing migrations, warn about resetting
|
|
339
348
|
if (snapshotExists || migrations.length > 0) {
|
|
340
349
|
ctx.log("warning", "You should run 'zenstack-kit init' to reset the snapshot after reviewing the schema.");
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export interface LoadedConfig {
|
|
|
7
7
|
configPath: string;
|
|
8
8
|
configDir: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function loadConfig(cwd: string): Promise<LoadedConfig | null>;
|
|
10
|
+
export declare function loadConfig(cwd: string, configPath?: string): Promise<LoadedConfig | null>;
|
|
11
11
|
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA+B/F"}
|
package/dist/config/loader.js
CHANGED
|
@@ -10,32 +10,34 @@ const CONFIG_FILES = [
|
|
|
10
10
|
"zenstack-kit.config.mjs",
|
|
11
11
|
"zenstack-kit.config.cjs",
|
|
12
12
|
];
|
|
13
|
-
export async function loadConfig(cwd) {
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
export async function loadConfig(cwd, configPath) {
|
|
14
|
+
const resolvedConfigPath = configPath ? path.resolve(cwd, configPath) : null;
|
|
15
|
+
const configPathToLoad = resolvedConfigPath ??
|
|
16
|
+
CONFIG_FILES.map((file) => path.join(cwd, file)).find((file) => fs.existsSync(file));
|
|
17
|
+
if (!configPathToLoad) {
|
|
16
18
|
return null;
|
|
17
19
|
}
|
|
18
|
-
const ext = path.extname(
|
|
20
|
+
const ext = path.extname(configPathToLoad);
|
|
19
21
|
let config;
|
|
20
22
|
if (ext === ".cjs") {
|
|
21
23
|
const require = createRequire(import.meta.url);
|
|
22
|
-
const loaded = require(
|
|
24
|
+
const loaded = require(configPathToLoad);
|
|
23
25
|
config = (loaded.default ?? loaded);
|
|
24
26
|
}
|
|
25
27
|
else if (ext === ".js" || ext === ".mjs") {
|
|
26
|
-
const loaded = await import(pathToFileUrl(
|
|
28
|
+
const loaded = await import(pathToFileUrl(configPathToLoad));
|
|
27
29
|
config = (loaded.default ?? loaded);
|
|
28
30
|
}
|
|
29
31
|
else {
|
|
30
32
|
const { default: jiti } = await import("jiti");
|
|
31
33
|
const loader = jiti(import.meta.url, { interopDefault: true });
|
|
32
|
-
const loaded = loader(
|
|
34
|
+
const loaded = loader(configPathToLoad);
|
|
33
35
|
config = (loaded.default ?? loaded);
|
|
34
36
|
}
|
|
35
37
|
return {
|
|
36
38
|
config,
|
|
37
|
-
configPath,
|
|
38
|
-
configDir: path.dirname(
|
|
39
|
+
configPath: configPathToLoad,
|
|
40
|
+
configDir: path.dirname(configPathToLoad),
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
function pathToFileUrl(filePath) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { KyselyDialect } from "../../sql/kysely-adapter.js";
|
|
2
|
+
export interface ApplyPrismaMigrationsOptions {
|
|
3
|
+
/** Migrations folder path */
|
|
4
|
+
migrationsFolder: string;
|
|
5
|
+
/** Database dialect */
|
|
6
|
+
dialect: KyselyDialect;
|
|
7
|
+
/** Database connection URL */
|
|
8
|
+
connectionUrl?: string;
|
|
9
|
+
/** SQLite database path */
|
|
10
|
+
databasePath?: string;
|
|
11
|
+
/** Migrations table name (default: _prisma_migrations) */
|
|
12
|
+
migrationsTable?: string;
|
|
13
|
+
/** Migrations schema (PostgreSQL only, default: public) */
|
|
14
|
+
migrationsSchema?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ApplyPrismaMigrationsResult {
|
|
17
|
+
applied: Array<{
|
|
18
|
+
migrationName: string;
|
|
19
|
+
duration: number;
|
|
20
|
+
}>;
|
|
21
|
+
alreadyApplied: string[];
|
|
22
|
+
failed?: {
|
|
23
|
+
migrationName: string;
|
|
24
|
+
error: string;
|
|
25
|
+
};
|
|
26
|
+
coherenceErrors?: MigrationCoherenceError[];
|
|
27
|
+
}
|
|
28
|
+
export interface MigrationCoherenceError {
|
|
29
|
+
type: "missing_from_log" | "missing_from_db" | "missing_from_disk" | "order_mismatch" | "checksum_mismatch";
|
|
30
|
+
migrationName: string;
|
|
31
|
+
details: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MigrationCoherenceResult {
|
|
34
|
+
isCoherent: boolean;
|
|
35
|
+
errors: MigrationCoherenceError[];
|
|
36
|
+
}
|
|
37
|
+
export interface PreviewPrismaMigrationsResult {
|
|
38
|
+
pending: Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
sql: string;
|
|
41
|
+
}>;
|
|
42
|
+
alreadyApplied: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Apply pending Prisma migrations
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyPrismaMigrations(options: ApplyPrismaMigrationsOptions): Promise<ApplyPrismaMigrationsResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Preview pending migrations without applying them
|
|
50
|
+
*/
|
|
51
|
+
export declare function previewPrismaMigrations(options: ApplyPrismaMigrationsOptions): Promise<PreviewPrismaMigrationsResult>;
|
|
52
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/migrations/prisma/apply.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAIjE,MAAM,WAAW,4BAA4B;IAC3C,6BAA6B;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,uBAAuB;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,KAAK,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,eAAe,CAAC,EAAE,uBAAuB,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;IAC5G,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,uBAAuB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AA2QD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAuHtC;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,6BAA6B,CAAC,CA0DxC"}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as crypto from "crypto";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
import { createKyselyAdapter } from "../../sql/kysely-adapter.js";
|
|
6
|
+
import { calculateChecksum, readMigrationLog } from "./log.js";
|
|
7
|
+
/**
|
|
8
|
+
* Ensure _prisma_migrations table exists
|
|
9
|
+
*/
|
|
10
|
+
async function ensureMigrationsTable(db, tableName, schema, dialect) {
|
|
11
|
+
const fullTableName = schema && dialect === "postgres" ? `${schema}.${tableName}` : tableName;
|
|
12
|
+
if (dialect === "sqlite") {
|
|
13
|
+
await sql `
|
|
14
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(`"${tableName}"`)} (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
checksum TEXT NOT NULL,
|
|
17
|
+
finished_at TEXT,
|
|
18
|
+
migration_name TEXT NOT NULL,
|
|
19
|
+
logs TEXT,
|
|
20
|
+
rolled_back_at TEXT,
|
|
21
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
22
|
+
applied_steps_count INTEGER NOT NULL DEFAULT 0
|
|
23
|
+
)
|
|
24
|
+
`.execute(db);
|
|
25
|
+
}
|
|
26
|
+
else if (dialect === "postgres") {
|
|
27
|
+
await sql `
|
|
28
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(`"${schema}"."${tableName}"`)} (
|
|
29
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
30
|
+
checksum VARCHAR(64) NOT NULL,
|
|
31
|
+
finished_at TIMESTAMPTZ,
|
|
32
|
+
migration_name VARCHAR(255) NOT NULL,
|
|
33
|
+
logs TEXT,
|
|
34
|
+
rolled_back_at TIMESTAMPTZ,
|
|
35
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
36
|
+
applied_steps_count INTEGER NOT NULL DEFAULT 0
|
|
37
|
+
)
|
|
38
|
+
`.execute(db);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
await sql `
|
|
42
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(`\`${tableName}\``)} (
|
|
43
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
44
|
+
checksum VARCHAR(64) NOT NULL,
|
|
45
|
+
finished_at DATETIME,
|
|
46
|
+
migration_name VARCHAR(255) NOT NULL,
|
|
47
|
+
logs TEXT,
|
|
48
|
+
rolled_back_at DATETIME,
|
|
49
|
+
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
50
|
+
applied_steps_count INTEGER NOT NULL DEFAULT 0
|
|
51
|
+
)
|
|
52
|
+
`.execute(db);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get list of applied migrations from _prisma_migrations table
|
|
57
|
+
*/
|
|
58
|
+
async function getAppliedMigrations(db, tableName, schema, dialect) {
|
|
59
|
+
let result;
|
|
60
|
+
if (dialect === "postgres" && schema) {
|
|
61
|
+
result = await sql `
|
|
62
|
+
SELECT * FROM ${sql.raw(`"${schema}"."${tableName}"`)}
|
|
63
|
+
WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
|
|
64
|
+
`.execute(db);
|
|
65
|
+
}
|
|
66
|
+
else if (dialect === "sqlite") {
|
|
67
|
+
result = await sql `
|
|
68
|
+
SELECT * FROM ${sql.raw(`"${tableName}"`)}
|
|
69
|
+
WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
|
|
70
|
+
`.execute(db);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result = await sql `
|
|
74
|
+
SELECT * FROM ${sql.raw(`\`${tableName}\``)}
|
|
75
|
+
WHERE rolled_back_at IS NULL AND finished_at IS NOT NULL
|
|
76
|
+
`.execute(db);
|
|
77
|
+
}
|
|
78
|
+
return new Map(result.rows.map((row) => [row.migration_name, row]));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Record a migration in _prisma_migrations table
|
|
82
|
+
*/
|
|
83
|
+
async function recordMigration(db, tableName, schema, dialect, migrationName, checksum) {
|
|
84
|
+
const id = crypto.randomUUID();
|
|
85
|
+
if (dialect === "postgres" && schema) {
|
|
86
|
+
await sql `
|
|
87
|
+
INSERT INTO ${sql.raw(`"${schema}"."${tableName}"`)} (id, checksum, migration_name, finished_at, applied_steps_count)
|
|
88
|
+
VALUES (${id}, ${checksum}, ${migrationName}, now(), 1)
|
|
89
|
+
`.execute(db);
|
|
90
|
+
}
|
|
91
|
+
else if (dialect === "sqlite") {
|
|
92
|
+
await sql `
|
|
93
|
+
INSERT INTO ${sql.raw(`"${tableName}"`)} (id, checksum, migration_name, finished_at, applied_steps_count)
|
|
94
|
+
VALUES (${id}, ${checksum}, ${migrationName}, datetime('now'), 1)
|
|
95
|
+
`.execute(db);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await sql `
|
|
99
|
+
INSERT INTO ${sql.raw(`\`${tableName}\``)} (id, checksum, migration_name, finished_at, applied_steps_count)
|
|
100
|
+
VALUES (${id}, ${checksum}, ${migrationName}, NOW(), 1)
|
|
101
|
+
`.execute(db);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validate that the database's applied migrations are coherent with the migration log.
|
|
106
|
+
*
|
|
107
|
+
* Coherence rules:
|
|
108
|
+
* 1. Every migration applied in the DB must exist in the migration log
|
|
109
|
+
* 2. Applied migrations must be a prefix of the log (no gaps)
|
|
110
|
+
* 3. Checksums must match for applied migrations
|
|
111
|
+
*/
|
|
112
|
+
function validateMigrationCoherence(appliedMigrations, migrationLog, migrationFolders) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
// Build a set of log migration names for quick lookup
|
|
115
|
+
const logMigrationNames = new Set(migrationLog.map((e) => e.name));
|
|
116
|
+
const folderNames = new Set(migrationFolders);
|
|
117
|
+
for (const entry of migrationLog) {
|
|
118
|
+
if (!folderNames.has(entry.name)) {
|
|
119
|
+
errors.push({
|
|
120
|
+
type: "missing_from_disk",
|
|
121
|
+
migrationName: entry.name,
|
|
122
|
+
details: `Migration "${entry.name}" exists in migration log but not on disk`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Check 1: Every applied migration must exist in the log
|
|
127
|
+
for (const [migrationName] of appliedMigrations) {
|
|
128
|
+
if (!logMigrationNames.has(migrationName)) {
|
|
129
|
+
errors.push({
|
|
130
|
+
type: "missing_from_log",
|
|
131
|
+
migrationName,
|
|
132
|
+
details: `Migration "${migrationName}" exists in database but not in migration log`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// If there are migrations missing from the log, return early
|
|
137
|
+
// (other checks don't make sense if the log is incomplete)
|
|
138
|
+
if (errors.length > 0) {
|
|
139
|
+
return { isCoherent: false, errors };
|
|
140
|
+
}
|
|
141
|
+
// Check 2: Applied migrations should be a continuous prefix of the log
|
|
142
|
+
// i.e., if migration N is applied, all migrations before N in the log must also be applied
|
|
143
|
+
let lastAppliedIndex = -1;
|
|
144
|
+
for (let i = 0; i < migrationLog.length; i++) {
|
|
145
|
+
const logEntry = migrationLog[i];
|
|
146
|
+
const isApplied = appliedMigrations.has(logEntry.name);
|
|
147
|
+
if (isApplied) {
|
|
148
|
+
// Check for gaps: if this is applied, all previous should be applied
|
|
149
|
+
if (lastAppliedIndex !== i - 1) {
|
|
150
|
+
// There's a gap - find the missing migrations
|
|
151
|
+
for (let j = lastAppliedIndex + 1; j < i; j++) {
|
|
152
|
+
const missing = migrationLog[j];
|
|
153
|
+
errors.push({
|
|
154
|
+
type: "order_mismatch",
|
|
155
|
+
migrationName: missing.name,
|
|
156
|
+
details: `Migration "${missing.name}" is in the log but not applied, yet later migration "${logEntry.name}" is applied`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
lastAppliedIndex = i;
|
|
161
|
+
// Check 3: Checksum validation for applied migrations
|
|
162
|
+
const dbRow = appliedMigrations.get(logEntry.name);
|
|
163
|
+
if (dbRow.checksum !== logEntry.checksum) {
|
|
164
|
+
errors.push({
|
|
165
|
+
type: "checksum_mismatch",
|
|
166
|
+
migrationName: logEntry.name,
|
|
167
|
+
details: `Checksum mismatch for "${logEntry.name}": database has ${dbRow.checksum.slice(0, 8)}..., log has ${logEntry.checksum.slice(0, 8)}...`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
isCoherent: errors.length === 0,
|
|
174
|
+
errors,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Execute raw SQL using the database driver directly
|
|
179
|
+
* This bypasses Kysely for DDL statements which don't work reliably with sql.raw()
|
|
180
|
+
*/
|
|
181
|
+
async function executeRawSql(dialect, sqlContent, options) {
|
|
182
|
+
if (dialect === "sqlite") {
|
|
183
|
+
const { default: Database } = await import("better-sqlite3");
|
|
184
|
+
const sqliteDb = new Database(options.databasePath || ":memory:");
|
|
185
|
+
try {
|
|
186
|
+
// better-sqlite3's exec() handles multiple statements properly
|
|
187
|
+
sqliteDb.exec(sqlContent);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
sqliteDb.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (dialect === "postgres") {
|
|
194
|
+
const { Pool } = await import("pg");
|
|
195
|
+
const pool = new Pool({ connectionString: options.connectionUrl });
|
|
196
|
+
const client = await pool.connect();
|
|
197
|
+
try {
|
|
198
|
+
// PostgreSQL supports transactional DDL, so wrap migration in a transaction
|
|
199
|
+
await client.query("BEGIN");
|
|
200
|
+
await client.query(sqlContent);
|
|
201
|
+
await client.query("COMMIT");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
await client.query("ROLLBACK");
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
client.release();
|
|
209
|
+
await pool.end();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (dialect === "mysql") {
|
|
213
|
+
// Use mysql2 with promise wrapper
|
|
214
|
+
const mysql = await import("mysql2");
|
|
215
|
+
const pool = mysql.createPool({ uri: options.connectionUrl });
|
|
216
|
+
const promisePool = pool.promise();
|
|
217
|
+
try {
|
|
218
|
+
// MySQL needs statements executed one at a time
|
|
219
|
+
const statements = sqlContent
|
|
220
|
+
.split(/;(?:\s*\n|\s*$)/)
|
|
221
|
+
.map((s) => s.trim())
|
|
222
|
+
.filter((s) => s.length > 0 && !s.startsWith("--"));
|
|
223
|
+
for (const statement of statements) {
|
|
224
|
+
await promisePool.query(statement);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await pool.promise().end();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Apply pending Prisma migrations
|
|
234
|
+
*/
|
|
235
|
+
export async function applyPrismaMigrations(options) {
|
|
236
|
+
const migrationsTable = options.migrationsTable ?? "_prisma_migrations";
|
|
237
|
+
const migrationsSchema = options.migrationsSchema ?? "public";
|
|
238
|
+
const { db, destroy } = await createKyselyAdapter({
|
|
239
|
+
dialect: options.dialect,
|
|
240
|
+
connectionUrl: options.connectionUrl,
|
|
241
|
+
databasePath: options.databasePath,
|
|
242
|
+
});
|
|
243
|
+
try {
|
|
244
|
+
// Ensure migrations table exists
|
|
245
|
+
await ensureMigrationsTable(db, migrationsTable, migrationsSchema, options.dialect);
|
|
246
|
+
// Get already applied migrations
|
|
247
|
+
const appliedMigrations = await getAppliedMigrations(db, migrationsTable, migrationsSchema, options.dialect);
|
|
248
|
+
// Read migration folders
|
|
249
|
+
const entries = await fs.readdir(options.migrationsFolder, { withFileTypes: true });
|
|
250
|
+
const migrationFolders = entries
|
|
251
|
+
.filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
|
|
252
|
+
.map((e) => e.name)
|
|
253
|
+
.sort();
|
|
254
|
+
const migrationFoldersWithSql = [];
|
|
255
|
+
for (const folderName of migrationFolders) {
|
|
256
|
+
const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
|
|
257
|
+
try {
|
|
258
|
+
await fs.access(sqlPath);
|
|
259
|
+
migrationFoldersWithSql.push(folderName);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Missing migration.sql; coherence check will flag if it's in the log
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Read migration log and validate coherence
|
|
266
|
+
const migrationLog = await readMigrationLog(options.migrationsFolder);
|
|
267
|
+
const coherence = validateMigrationCoherence(appliedMigrations, migrationLog, migrationFoldersWithSql);
|
|
268
|
+
if (!coherence.isCoherent) {
|
|
269
|
+
return {
|
|
270
|
+
applied: [],
|
|
271
|
+
alreadyApplied: [],
|
|
272
|
+
coherenceErrors: coherence.errors,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const result = {
|
|
276
|
+
applied: [],
|
|
277
|
+
alreadyApplied: [],
|
|
278
|
+
};
|
|
279
|
+
for (const folderName of migrationFoldersWithSql) {
|
|
280
|
+
if (appliedMigrations.has(folderName)) {
|
|
281
|
+
result.alreadyApplied.push(folderName);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
|
|
285
|
+
let sqlContent;
|
|
286
|
+
try {
|
|
287
|
+
sqlContent = await fs.readFile(sqlPath, "utf-8");
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
continue; // Skip if no migration.sql
|
|
291
|
+
}
|
|
292
|
+
const checksum = calculateChecksum(sqlContent);
|
|
293
|
+
// Verify checksum against migration log (migrationLog already read above)
|
|
294
|
+
const logEntry = migrationLog.find((m) => m.name === folderName);
|
|
295
|
+
if (logEntry && logEntry.checksum !== checksum) {
|
|
296
|
+
result.failed = {
|
|
297
|
+
migrationName: folderName,
|
|
298
|
+
error: `Checksum mismatch for migration ${folderName}.\n` +
|
|
299
|
+
`Expected: ${logEntry.checksum}\n` +
|
|
300
|
+
`Found: ${checksum}\n` +
|
|
301
|
+
`The migration file may have been modified after generation.`,
|
|
302
|
+
};
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
const startTime = Date.now();
|
|
306
|
+
try {
|
|
307
|
+
// Execute the migration SQL using direct driver access
|
|
308
|
+
await executeRawSql(options.dialect, sqlContent, {
|
|
309
|
+
connectionUrl: options.connectionUrl,
|
|
310
|
+
databasePath: options.databasePath,
|
|
311
|
+
});
|
|
312
|
+
// Record the migration (still use Kysely for this since it's simple INSERT)
|
|
313
|
+
await recordMigration(db, migrationsTable, migrationsSchema, options.dialect, folderName, checksum);
|
|
314
|
+
result.applied.push({
|
|
315
|
+
migrationName: folderName,
|
|
316
|
+
duration: Date.now() - startTime,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
result.failed = {
|
|
321
|
+
migrationName: folderName,
|
|
322
|
+
error: error instanceof Error ? error.message : String(error),
|
|
323
|
+
};
|
|
324
|
+
break; // Stop on first failure
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
await destroy();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Preview pending migrations without applying them
|
|
335
|
+
*/
|
|
336
|
+
export async function previewPrismaMigrations(options) {
|
|
337
|
+
const migrationsTable = options.migrationsTable ?? "_prisma_migrations";
|
|
338
|
+
const migrationsSchema = options.migrationsSchema ?? "public";
|
|
339
|
+
const { db, destroy } = await createKyselyAdapter({
|
|
340
|
+
dialect: options.dialect,
|
|
341
|
+
connectionUrl: options.connectionUrl,
|
|
342
|
+
databasePath: options.databasePath,
|
|
343
|
+
});
|
|
344
|
+
try {
|
|
345
|
+
// Ensure migrations table exists
|
|
346
|
+
await ensureMigrationsTable(db, migrationsTable, migrationsSchema, options.dialect);
|
|
347
|
+
// Get already applied migrations
|
|
348
|
+
const appliedMigrations = await getAppliedMigrations(db, migrationsTable, migrationsSchema, options.dialect);
|
|
349
|
+
// Read migration folders
|
|
350
|
+
const entries = await fs.readdir(options.migrationsFolder, { withFileTypes: true });
|
|
351
|
+
const migrationFolders = entries
|
|
352
|
+
.filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
|
|
353
|
+
.map((e) => e.name)
|
|
354
|
+
.sort();
|
|
355
|
+
const result = {
|
|
356
|
+
pending: [],
|
|
357
|
+
alreadyApplied: [],
|
|
358
|
+
};
|
|
359
|
+
for (const folderName of migrationFolders) {
|
|
360
|
+
if (appliedMigrations.has(folderName)) {
|
|
361
|
+
result.alreadyApplied.push(folderName);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const sqlPath = path.join(options.migrationsFolder, folderName, "migration.sql");
|
|
365
|
+
let sqlContent;
|
|
366
|
+
try {
|
|
367
|
+
sqlContent = await fs.readFile(sqlPath, "utf-8");
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue; // Skip if no migration.sql
|
|
371
|
+
}
|
|
372
|
+
result.pending.push({
|
|
373
|
+
name: folderName,
|
|
374
|
+
sql: sqlContent,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
await destroy();
|
|
381
|
+
}
|
|
382
|
+
}
|