gencow 0.1.189 → 0.1.190
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/core/index.js +34 -12
- package/lib/app-command.mjs +19 -5
- package/lib/app-response-contract.mjs +11 -7
- package/lib/canonical-drizzle-generator.config.mjs +61 -0
- package/lib/cli-artifact-guard.mjs +29 -0
- package/lib/cli-project-runtime.mjs +18 -3
- package/lib/db-push-command.mjs +2 -1
- package/lib/deploy-failure-diagnostic.mjs +37 -9
- package/lib/deploy-package-claim.mjs +24 -6
- package/lib/deploy-package-runtime.mjs +16 -7
- package/lib/dev-cloud-command.mjs +17 -2
- package/lib/drizzle-generate.mjs +92 -8
- package/lib/drizzle-generator-invocation.mjs +23 -0
- package/lib/project-migration-manifest.mjs +0 -1
- package/package.json +4 -3
- package/runtime/server.mjs +63289 -16972
- package/runtime/tooling.mjs +4 -0
- package/scripts/bundle-server.mjs +52 -12
- package/scripts/pre-publish-check.mjs +20 -0
- package/runtime/server.mjs.map +0 -7
package/core/index.js
CHANGED
|
@@ -8,10 +8,12 @@ function hasStandardSchema(schema) {
|
|
|
8
8
|
return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
|
|
9
9
|
}
|
|
10
10
|
var HttpRouteInputValidationError = class extends Error {
|
|
11
|
-
constructor(
|
|
12
|
-
super(message);
|
|
11
|
+
constructor(issues) {
|
|
12
|
+
super(issues[0]?.message ?? "Invalid input");
|
|
13
|
+
this.issues = issues;
|
|
13
14
|
this.name = "HttpRouteInputValidationError";
|
|
14
15
|
}
|
|
16
|
+
issues;
|
|
15
17
|
};
|
|
16
18
|
var HttpRouteOutputValidationError = class extends Error {
|
|
17
19
|
constructor(message) {
|
|
@@ -26,10 +28,16 @@ async function validateInput(schema, value) {
|
|
|
26
28
|
}
|
|
27
29
|
const result = await schema["~standard"].validate(value);
|
|
28
30
|
if ("issues" in result && result.issues.length > 0) {
|
|
29
|
-
throw new HttpRouteInputValidationError(
|
|
31
|
+
throw new HttpRouteInputValidationError(
|
|
32
|
+
result.issues.map((issue) => ({
|
|
33
|
+
message: issue.message,
|
|
34
|
+
path: issue.path?.map(String),
|
|
35
|
+
code: issue.code === void 0 ? void 0 : String(issue.code)
|
|
36
|
+
}))
|
|
37
|
+
);
|
|
30
38
|
}
|
|
31
39
|
if ("value" in result) return result.value;
|
|
32
|
-
throw new HttpRouteInputValidationError("Invalid input");
|
|
40
|
+
throw new HttpRouteInputValidationError([{ message: "Invalid input" }]);
|
|
33
41
|
}
|
|
34
42
|
async function validateOutput(schema, value) {
|
|
35
43
|
if (!schema) return value;
|
|
@@ -50,7 +58,7 @@ async function buildInputFromRequest(method, req) {
|
|
|
50
58
|
return await req.json();
|
|
51
59
|
} catch (err) {
|
|
52
60
|
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
-
throw new HttpRouteInputValidationError(`Invalid JSON body: ${message}`);
|
|
61
|
+
throw new HttpRouteInputValidationError([{ message: `Invalid JSON body: ${message}` }]);
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
return { ...req.query };
|
|
@@ -105,7 +113,7 @@ function composeRouteMiddlewares(middlewares, handler, inputSchema, outputSchema
|
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
108
|
-
constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false) {
|
|
116
|
+
constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false, inputErrorMapper) {
|
|
109
117
|
this.method = method;
|
|
110
118
|
this.routePath = routePath;
|
|
111
119
|
this.middlewares = middlewares;
|
|
@@ -114,6 +122,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
114
122
|
this.inputValidationIndex = inputValidationIndex;
|
|
115
123
|
this.outputValidationIndex = outputValidationIndex;
|
|
116
124
|
this.anonymousAllowed = anonymousAllowed;
|
|
125
|
+
this.inputErrorMapper = inputErrorMapper;
|
|
117
126
|
}
|
|
118
127
|
method;
|
|
119
128
|
routePath;
|
|
@@ -123,6 +132,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
123
132
|
inputValidationIndex;
|
|
124
133
|
outputValidationIndex;
|
|
125
134
|
anonymousAllowed;
|
|
135
|
+
inputErrorMapper;
|
|
126
136
|
path(path) {
|
|
127
137
|
return new _HttpRouteBuilderImpl(
|
|
128
138
|
this.method,
|
|
@@ -132,7 +142,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
132
142
|
this.outputSchema,
|
|
133
143
|
this.inputValidationIndex,
|
|
134
144
|
this.outputValidationIndex,
|
|
135
|
-
this.anonymousAllowed
|
|
145
|
+
this.anonymousAllowed,
|
|
146
|
+
this.inputErrorMapper
|
|
136
147
|
);
|
|
137
148
|
}
|
|
138
149
|
allowAnonymous() {
|
|
@@ -144,7 +155,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
144
155
|
this.outputSchema,
|
|
145
156
|
this.inputValidationIndex,
|
|
146
157
|
this.outputValidationIndex,
|
|
147
|
-
true
|
|
158
|
+
true,
|
|
159
|
+
this.inputErrorMapper
|
|
148
160
|
);
|
|
149
161
|
}
|
|
150
162
|
use(middleware) {
|
|
@@ -156,10 +168,11 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
156
168
|
this.outputSchema,
|
|
157
169
|
this.inputValidationIndex,
|
|
158
170
|
this.outputValidationIndex,
|
|
159
|
-
this.anonymousAllowed
|
|
171
|
+
this.anonymousAllowed,
|
|
172
|
+
this.inputErrorMapper
|
|
160
173
|
);
|
|
161
174
|
}
|
|
162
|
-
input(schema) {
|
|
175
|
+
input(schema, options) {
|
|
163
176
|
const nextInputValidationIndex = this.middlewares.length;
|
|
164
177
|
return new _HttpRouteBuilderImpl(
|
|
165
178
|
this.method,
|
|
@@ -169,7 +182,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
169
182
|
this.outputSchema,
|
|
170
183
|
nextInputValidationIndex,
|
|
171
184
|
this.outputValidationIndex,
|
|
172
|
-
this.anonymousAllowed
|
|
185
|
+
this.anonymousAllowed,
|
|
186
|
+
options?.onError
|
|
173
187
|
);
|
|
174
188
|
}
|
|
175
189
|
output(schema) {
|
|
@@ -182,7 +196,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
182
196
|
schema,
|
|
183
197
|
this.inputValidationIndex,
|
|
184
198
|
nextOutputValidationIndex,
|
|
185
|
-
this.anonymousAllowed
|
|
199
|
+
this.anonymousAllowed,
|
|
200
|
+
this.inputErrorMapper
|
|
186
201
|
);
|
|
187
202
|
}
|
|
188
203
|
handler(handler) {
|
|
@@ -194,6 +209,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
194
209
|
const isPublic = this.anonymousAllowed;
|
|
195
210
|
const inputSchema = this.inputSchema;
|
|
196
211
|
const outputSchema = this.outputSchema;
|
|
212
|
+
const inputErrorMapper = this.inputErrorMapper;
|
|
197
213
|
const runRoute = composeRouteMiddlewares(
|
|
198
214
|
this.middlewares,
|
|
199
215
|
handler,
|
|
@@ -208,6 +224,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
208
224
|
normalizedInput = inputSchema ? await buildInputFromRequest(method, req) : void 0;
|
|
209
225
|
} catch (err) {
|
|
210
226
|
if (err instanceof HttpRouteInputValidationError) {
|
|
227
|
+
if (inputErrorMapper) return await inputErrorMapper(err.issues);
|
|
211
228
|
return { status: 400, body: { error: err.message } };
|
|
212
229
|
}
|
|
213
230
|
throw err;
|
|
@@ -217,6 +234,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
217
234
|
result = await runRoute(ctx, normalizedInput, req);
|
|
218
235
|
} catch (err) {
|
|
219
236
|
if (err instanceof HttpRouteInputValidationError) {
|
|
237
|
+
if (inputErrorMapper) return await inputErrorMapper(err.issues);
|
|
220
238
|
return { status: 400, body: { error: err.message } };
|
|
221
239
|
}
|
|
222
240
|
if (err instanceof HttpRouteOutputValidationError) {
|
|
@@ -3006,6 +3024,10 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
3006
3024
|
"PGBOUNCER_PORT",
|
|
3007
3025
|
"DIRECT_PG_PORT",
|
|
3008
3026
|
"GENCOW_DIRECT_DATABASE_URL",
|
|
3027
|
+
"GENCOW_BACKUP_DATABASE_URL",
|
|
3028
|
+
"GENCOW_BACKUP_EXPECTED_ROLE",
|
|
3029
|
+
"GENCOW_BACKUP_RLS_BYPASS_APPROVED",
|
|
3030
|
+
"GENCOW_RESTORE_DATABASE_URL",
|
|
3009
3031
|
"GENCOW_CATALOG_READ_DATABASE_URL",
|
|
3010
3032
|
"GENCOW_DIRECT_DDL_DATABASE_URL",
|
|
3011
3033
|
"DIRECT_DATABASE_URL",
|
package/lib/app-command.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-res
|
|
|
10
10
|
import {
|
|
11
11
|
isExactAppDeleteResponse,
|
|
12
12
|
parseAppListResponse,
|
|
13
|
-
|
|
13
|
+
parseAppLookupResponse,
|
|
14
14
|
} from "./app-response-contract.mjs";
|
|
15
15
|
import { pollAppDeleteOperation } from "./app-delete-operation.mjs";
|
|
16
16
|
import { appResponseError } from "./app-response-error.mjs";
|
|
@@ -32,6 +32,10 @@ function invalidAppResponse(operation, response, kind) {
|
|
|
32
32
|
return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function appNotFoundError() {
|
|
36
|
+
return "App not found or not owned by you.\nCode: APP_NOT_FOUND";
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
export function formatAppDeployedAgo(dateStr, now = Date.now()) {
|
|
36
40
|
if (!dateStr) return `${DIM}never${RESET}`;
|
|
37
41
|
const diff = now - new Date(dateStr).getTime();
|
|
@@ -322,22 +326,32 @@ ${dashboardLine}
|
|
|
322
326
|
const res = await rpcQueryImpl(creds, "apps.get", { name });
|
|
323
327
|
const body = await readJsonObjectResponse(res);
|
|
324
328
|
if (!res.ok) {
|
|
325
|
-
errorImpl(
|
|
329
|
+
errorImpl(
|
|
330
|
+
res.status === 404
|
|
331
|
+
? appNotFoundError()
|
|
332
|
+
: appResponseError(body, invalidAppResponse("app status", res, "error")),
|
|
333
|
+
);
|
|
326
334
|
processRef.exit(1);
|
|
327
335
|
return;
|
|
328
336
|
}
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
337
|
+
const lookup = parseAppLookupResponse(body, name);
|
|
338
|
+
if (lookup.kind === "not_found") {
|
|
339
|
+
errorImpl(appNotFoundError());
|
|
340
|
+
processRef.exit(1);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (lookup.kind === "invalid_response") {
|
|
331
344
|
errorImpl(invalidAppResponse("app status", res, "success"));
|
|
332
345
|
processRef.exit(1);
|
|
333
346
|
return;
|
|
334
347
|
}
|
|
348
|
+
const data = lookup.app;
|
|
335
349
|
logImpl(`\n ${BOLD}${data.name}${RESET}`);
|
|
336
350
|
const status = formatAppStatusForCli(data);
|
|
337
351
|
logImpl(` Status: ${status.colored}`);
|
|
338
352
|
const memoryExceededDetails = formatMemoryExceededDetails(data);
|
|
339
353
|
if (memoryExceededDetails) logImpl(memoryExceededDetails);
|
|
340
|
-
logImpl(` Port: ${data.port}`);
|
|
354
|
+
logImpl(` Port: ${data.port ?? `${DIM}not deployed${RESET}`}`);
|
|
341
355
|
if (isAppFailureStatus(data)) {
|
|
342
356
|
let diagnosticLines = selectAppDiagnosticLogLines(data);
|
|
343
357
|
if (diagnosticLines.length === 0) {
|
|
@@ -25,22 +25,26 @@ export function parseAppListResponse(value) {
|
|
|
25
25
|
: null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function parseAppLookupResponse(value, expectedAppId) {
|
|
29
29
|
const body = record(value);
|
|
30
|
-
if (!body) return
|
|
30
|
+
if (!body) return { kind: "invalid_response" };
|
|
31
31
|
const hasEnvelope = Object.prototype.hasOwnProperty.call(body, "result");
|
|
32
|
+
if (hasEnvelope && body.result == null) return { kind: "not_found" };
|
|
32
33
|
const app = record(hasEnvelope ? body.result : body);
|
|
33
34
|
if (
|
|
34
35
|
!app ||
|
|
35
36
|
app.name !== expectedAppId ||
|
|
36
37
|
!nonEmptyString(app.status) ||
|
|
37
|
-
!Number.isSafeInteger(app.port) ||
|
|
38
|
-
app.port <= 0 ||
|
|
39
|
-
app.port > 65535
|
|
38
|
+
(app.port != null && (!Number.isSafeInteger(app.port) || app.port <= 0 || app.port > 65535))
|
|
40
39
|
) {
|
|
41
|
-
return
|
|
40
|
+
return { kind: "invalid_response" };
|
|
42
41
|
}
|
|
43
|
-
return app;
|
|
42
|
+
return { kind: "found", app };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseAppStatusResponse(value, expectedAppId) {
|
|
46
|
+
const result = parseAppLookupResponse(value, expectedAppId);
|
|
47
|
+
return result.kind === "found" ? result.app : null;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function isExactAppDeleteResponse(value, expectedAppId) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const GENCOW_DRIZZLE_CONFIG_CONTRACT_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
function requiredEnvironment(name) {
|
|
7
|
+
const value = process.env[name]?.trim();
|
|
8
|
+
if (!value) throw new Error(`Gencow migration generator requires ${name}`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function insideProject(projectRoot, candidate) {
|
|
13
|
+
const pathFromRoot = relative(projectRoot, candidate);
|
|
14
|
+
return pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalOutputPath(projectRoot, rawPath) {
|
|
18
|
+
const candidate = resolve(projectRoot, rawPath);
|
|
19
|
+
if (existsSync(candidate)) return realpathSync(candidate);
|
|
20
|
+
return resolve(realpathSync(dirname(candidate)), basename(candidate));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveSchemaFiles(projectRoot) {
|
|
24
|
+
const rawPaths = requiredEnvironment("GENCOW_SCHEMA_PATHS").split("\n");
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const rawPath of rawPaths) {
|
|
28
|
+
const trimmed = rawPath.trim();
|
|
29
|
+
if (!trimmed) continue;
|
|
30
|
+
const candidate = resolve(projectRoot, trimmed);
|
|
31
|
+
const stat = lstatSync(candidate);
|
|
32
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
33
|
+
throw new Error("Gencow migration schema must be a regular project file");
|
|
34
|
+
}
|
|
35
|
+
const canonical = realpathSync(candidate);
|
|
36
|
+
if (!insideProject(projectRoot, canonical)) {
|
|
37
|
+
throw new Error("Gencow migration schema must stay inside the project root");
|
|
38
|
+
}
|
|
39
|
+
if (!seen.has(canonical)) {
|
|
40
|
+
seen.add(canonical);
|
|
41
|
+
files.push(canonical);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (files.length === 0) throw new Error("Gencow migration schema list is empty");
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const projectRoot = realpathSync(process.cwd());
|
|
49
|
+
const migrationsPath = canonicalOutputPath(projectRoot, requiredEnvironment("GENCOW_MIGRATIONS"));
|
|
50
|
+
if (!insideProject(projectRoot, migrationsPath)) {
|
|
51
|
+
throw new Error("Gencow migrations output must stay inside the project root");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
dialect: "postgresql",
|
|
56
|
+
schema: resolveSchemaFiles(projectRoot),
|
|
57
|
+
out: `./${relative(projectRoot, migrationsPath) || "."}`,
|
|
58
|
+
tablesFilter: ["!_system_*", "!_gencow_*"],
|
|
59
|
+
driver: "pglite",
|
|
60
|
+
dbCredentials: { url: process.env.GENCOW_DB_URL || "./.gencow/data" },
|
|
61
|
+
};
|
|
@@ -5,6 +5,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
|
|
|
5
5
|
"lib/deploy-auditor.mjs",
|
|
6
6
|
"lib/cron-manifest.mjs",
|
|
7
7
|
"lib/dev-cloud-bundle.mjs",
|
|
8
|
+
"lib/canonical-drizzle-generator.config.mjs",
|
|
8
9
|
"runtime/tooling.mjs",
|
|
9
10
|
"runtime/server.mjs",
|
|
10
11
|
"templateFeature/ai/manifest.json",
|
|
@@ -17,6 +18,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
|
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
export const TOOLING_RUNTIME_BUNDLE_PATH = "runtime/tooling.mjs";
|
|
21
|
+
export const SERVER_RUNTIME_BUNDLE_PATH = "runtime/server.mjs";
|
|
20
22
|
|
|
21
23
|
const FORBIDDEN_TOOLING_RUNTIME_PATTERNS = Object.freeze([
|
|
22
24
|
{
|
|
@@ -38,6 +40,11 @@ const FORBIDDEN_CLI_TARBALL_FILES = Object.freeze([
|
|
|
38
40
|
path: "lib/codegen/index.mjs",
|
|
39
41
|
message: "legacy CLI codegen artifact는 npm tarball에 포함되면 안 됩니다",
|
|
40
42
|
},
|
|
43
|
+
{
|
|
44
|
+
code: "forbidden-runtime-source-map",
|
|
45
|
+
path: "runtime/server.mjs.map",
|
|
46
|
+
message: "server runtime source map은 npm tarball에 포함되면 안 됩니다",
|
|
47
|
+
},
|
|
41
48
|
]);
|
|
42
49
|
|
|
43
50
|
export function validateCliTarballFiles(files, requiredFiles = REQUIRED_CLI_TARBALL_FILES) {
|
|
@@ -93,6 +100,28 @@ export function validateGeneratedCodegenBundle(source, options = {}) {
|
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
export function validateServerRuntimeBundle(source, options = {}) {
|
|
104
|
+
const artifactPath = options.artifactPath ?? SERVER_RUNTIME_BUNDLE_PATH;
|
|
105
|
+
const repositorySourcePath = String.raw`(?:platform|server|route-gateway)[\\/]src[\\/][a-z0-9_./\\-]+\.(?:[cm]?[jt]s)`;
|
|
106
|
+
const repositorySourceImportPatterns = [
|
|
107
|
+
new RegExp("import\\(\\s*[`\"'][^`\"'\\n]{0,640}" + repositorySourcePath, "iu"),
|
|
108
|
+
new RegExp(
|
|
109
|
+
String.raw`(?:resolve|join|new\s+URL)\([^;\n]{0,640}(?:${repositorySourcePath}|["'](?:platform|server|route-gateway)["']\s*,\s*["']src["'])`,
|
|
110
|
+
"iu",
|
|
111
|
+
),
|
|
112
|
+
];
|
|
113
|
+
const errors = repositorySourceImportPatterns.some((pattern) => pattern.test(source))
|
|
114
|
+
? [
|
|
115
|
+
{
|
|
116
|
+
code: "repository-source-import",
|
|
117
|
+
path: artifactPath,
|
|
118
|
+
message: "server runtime이 npm package 밖 repository source를 참조합니다",
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
: [];
|
|
122
|
+
return { ok: errors.length === 0, errors };
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
export function formatArtifactGuardErrors(errors) {
|
|
97
126
|
return errors.map((error) => ` ❌ ${error.path} — ${error.message}`).join("\n");
|
|
98
127
|
}
|
|
@@ -18,6 +18,7 @@ export { readProjectDisplayName, updateEnvLocalUrl } from "./project-metadata-ru
|
|
|
18
18
|
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
19
19
|
import { hasWorkspaceConfigSourceRuntime } from "./runtime-mode.mjs";
|
|
20
20
|
import { SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION } from "./drizzle-generate.mjs";
|
|
21
|
+
import { buildDrizzleGeneratorInvocation } from "./drizzle-generator-invocation.mjs";
|
|
21
22
|
|
|
22
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
|
|
@@ -327,7 +328,11 @@ export async function loadConfig(options = {}) {
|
|
|
327
328
|
existsSyncImpl,
|
|
328
329
|
resolvePathImpl,
|
|
329
330
|
});
|
|
330
|
-
warnProjectConfigSelectionOnce({
|
|
331
|
+
warnProjectConfigSelectionOnce({
|
|
332
|
+
cwd: projectSelection.projectDir,
|
|
333
|
+
selection: configSelection,
|
|
334
|
+
warnImpl,
|
|
335
|
+
});
|
|
331
336
|
}
|
|
332
337
|
} catch (caught) {
|
|
333
338
|
if (throwOnInvalid) {
|
|
@@ -530,6 +535,10 @@ function ensureEsbuildConsistency(options = {}) {
|
|
|
530
535
|
|
|
531
536
|
export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
532
537
|
const {
|
|
538
|
+
canonicalConfigPath = resolve(
|
|
539
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
540
|
+
"canonical-drizzle-generator.config.mjs",
|
|
541
|
+
),
|
|
533
542
|
cwd = process.cwd(),
|
|
534
543
|
createRequireImpl = createRequire,
|
|
535
544
|
dirnameImpl = dirname,
|
|
@@ -538,7 +547,7 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
|
538
547
|
readFileSyncImpl = readFileSync,
|
|
539
548
|
resolvePathImpl = resolve,
|
|
540
549
|
} = options;
|
|
541
|
-
if (!/^[a-z][a-z0-9:-]
|
|
550
|
+
if (!/^[a-z][a-z0-9:-]*$/u.test(subcmd)) throw new Error(`Invalid drizzle-kit command: ${subcmd}`);
|
|
542
551
|
try {
|
|
543
552
|
const cliRequire = createRequireImpl(import.meta.url);
|
|
544
553
|
const packageJsonPath = resolvePathImpl(dirnameImpl(cliRequire.resolve("drizzle-kit")), "package.json");
|
|
@@ -554,8 +563,14 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
|
554
563
|
const resolvedBin = resolvePathImpl(dirnameImpl(packageJsonPath), binRelative);
|
|
555
564
|
if (!existsSyncImpl(resolvedBin)) throw new Error("bin file missing");
|
|
556
565
|
const env = ensureEsbuildConsistency({ ...options, cwd });
|
|
566
|
+
const invocation = buildDrizzleGeneratorInvocation({
|
|
567
|
+
configPath: canonicalConfigPath,
|
|
568
|
+
generatorBinPath: resolvedBin,
|
|
569
|
+
processExecPath,
|
|
570
|
+
subcommand: subcmd,
|
|
571
|
+
});
|
|
557
572
|
return {
|
|
558
|
-
|
|
573
|
+
...invocation,
|
|
559
574
|
env,
|
|
560
575
|
generatorVersion: packageJson.version,
|
|
561
576
|
};
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -392,10 +392,11 @@ function createDbMigrationCommand(deps, command) {
|
|
|
392
392
|
} else {
|
|
393
393
|
if (!jsonMode) deps.infoImpl("Generating schema migrations...");
|
|
394
394
|
try {
|
|
395
|
+
const generationEnv = deps.buildEnv(config, { cwd: deps.cwdImpl() });
|
|
395
396
|
generatorVersion = runDrizzleGenerateWithFallback({
|
|
396
397
|
cwd: deps.cwdImpl(),
|
|
397
398
|
drizzleKitCmdImpl: deps.drizzleKitCmdImpl,
|
|
398
|
-
env:
|
|
399
|
+
env: generationEnv,
|
|
399
400
|
execSyncImpl: deps.execSyncImpl,
|
|
400
401
|
stdio: jsonMode ? "pipe" : "inherit",
|
|
401
402
|
warnImpl: jsonMode ? () => {} : deps.warnImpl,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MIGRATION_NEXT_ACTION_KINDS } from "@gencow/migration-contract";
|
|
2
2
|
|
|
3
|
+
import { APP_DELETE_OPERATION_PATTERN } from "./app-response-error.mjs";
|
|
3
4
|
import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
|
|
4
5
|
|
|
5
6
|
const DEPLOY_FAILURE_STAGES = new Set([
|
|
@@ -13,10 +14,17 @@ const DEPLOY_FAILURE_STAGES = new Set([
|
|
|
13
14
|
"controller",
|
|
14
15
|
]);
|
|
15
16
|
const RETRY_CLASSES = new Set(["terminal-client", "retryable-infra", "platform-action"]);
|
|
16
|
-
const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION"]);
|
|
17
|
-
const STRUCTURED_CONTRACT_FIELDS = [
|
|
18
|
-
|
|
19
|
-
"
|
|
17
|
+
const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION", "VIEW_DELETE_STATUS"]);
|
|
18
|
+
const STRUCTURED_CONTRACT_FIELDS = [
|
|
19
|
+
"code",
|
|
20
|
+
"stage",
|
|
21
|
+
"retryClass",
|
|
22
|
+
"userAction",
|
|
23
|
+
"correlationId",
|
|
24
|
+
"operationId",
|
|
25
|
+
"appName",
|
|
26
|
+
];
|
|
27
|
+
const PLATFORM_RECOVERY_MESSAGE = "Gencow is recovering this deployment. Your app files do not need changes.";
|
|
20
28
|
|
|
21
29
|
function safeText(value, maxLength) {
|
|
22
30
|
if (typeof value !== "string") return null;
|
|
@@ -27,13 +35,14 @@ function safeText(value, maxLength) {
|
|
|
27
35
|
return normalized;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
function readDeployFailureContract(value) {
|
|
38
|
+
function readDeployFailureContract(value, expectedAppName) {
|
|
31
39
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
32
40
|
const code = safeText(value.code, 100);
|
|
33
41
|
const stage = safeText(value.stage, 20);
|
|
34
42
|
const retryClass = safeText(value.retryClass, 32);
|
|
35
43
|
const userAction = safeText(value.userAction, 32);
|
|
36
44
|
const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
|
|
45
|
+
const operationId = value.operationId === undefined ? null : safeText(value.operationId, 80);
|
|
37
46
|
if (
|
|
38
47
|
!code?.match(/^[A-Z][A-Z0-9_]{2,99}$/u) ||
|
|
39
48
|
!stage ||
|
|
@@ -43,7 +52,11 @@ function readDeployFailureContract(value) {
|
|
|
43
52
|
!userAction ||
|
|
44
53
|
!USER_ACTIONS.has(userAction) ||
|
|
45
54
|
(value.correlationId !== undefined && !correlationId) ||
|
|
46
|
-
(correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId))
|
|
55
|
+
(correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId)) ||
|
|
56
|
+
(userAction === "VIEW_DELETE_STATUS" &&
|
|
57
|
+
(!operationId ||
|
|
58
|
+
!APP_DELETE_OPERATION_PATTERN.test(operationId) ||
|
|
59
|
+
!/^[a-z][a-z0-9]*(?:-[a-z0-9]+){2}$/u.test(expectedAppName ?? "")))
|
|
47
60
|
) {
|
|
48
61
|
return null;
|
|
49
62
|
}
|
|
@@ -53,6 +66,7 @@ function readDeployFailureContract(value) {
|
|
|
53
66
|
retryClass,
|
|
54
67
|
userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
|
|
55
68
|
correlationId,
|
|
69
|
+
operationId,
|
|
56
70
|
legacySupportAction: userAction === "CONTACT_SUPPORT",
|
|
57
71
|
};
|
|
58
72
|
}
|
|
@@ -63,8 +77,8 @@ function normalizeLegacySupportMessage(message) {
|
|
|
63
77
|
: message;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
|
|
67
|
-
const contract = readDeployFailureContract(responseBody);
|
|
80
|
+
export function renderDeployFailureDiagnosticLines(responseBody, statusText, options = {}) {
|
|
81
|
+
const contract = readDeployFailureContract(responseBody, options.expectedAppName);
|
|
68
82
|
const fallbackMessage = safeText(statusText, 120) ?? "Request failed";
|
|
69
83
|
if (!contract) {
|
|
70
84
|
const structuredFields =
|
|
@@ -85,13 +99,27 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
|
|
|
85
99
|
if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
|
|
86
100
|
return lines;
|
|
87
101
|
}
|
|
88
|
-
const platformOwned =
|
|
102
|
+
const platformOwned =
|
|
103
|
+
contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
|
|
89
104
|
if (platformOwned) {
|
|
90
105
|
const lines = [`Deploy failed: ${PLATFORM_RECOVERY_MESSAGE}`];
|
|
91
106
|
const correlationLine = formatDeployCorrelationId(contract.correlationId);
|
|
92
107
|
if (correlationLine) lines.push(correlationLine);
|
|
93
108
|
return lines;
|
|
94
109
|
}
|
|
110
|
+
if (contract.userAction === "VIEW_DELETE_STATUS") {
|
|
111
|
+
const publicMessage = safeText(responseBody.error, 240) ?? fallbackMessage;
|
|
112
|
+
const lines = [
|
|
113
|
+
`Deploy failed: ${publicMessage}`,
|
|
114
|
+
`Code: ${contract.code}`,
|
|
115
|
+
`Stage: ${contract.stage}`,
|
|
116
|
+
`Operation ID: ${contract.operationId}`,
|
|
117
|
+
];
|
|
118
|
+
const correlationLine = formatDeployCorrelationId(contract.correlationId);
|
|
119
|
+
if (correlationLine) lines.push(correlationLine);
|
|
120
|
+
lines.push(`Next command: gencow app delete ${options.expectedAppName} --force`);
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
95
123
|
const publicMessage = contract.legacySupportAction
|
|
96
124
|
? PLATFORM_RECOVERY_MESSAGE
|
|
97
125
|
: (safeText(responseBody.error, 240) ?? fallbackMessage);
|
|
@@ -3,13 +3,10 @@ import { Readable } from "node:stream";
|
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { list } from "tar";
|
|
5
5
|
|
|
6
|
-
import { MIGRATION_BUNDLE_CAPABILITY_V1 } from "@gencow/migration-contract";
|
|
6
|
+
import { MIGRATION_BUNDLE_CAPABILITY_V1, inspectPnpmDependencyLock } from "@gencow/migration-contract";
|
|
7
7
|
|
|
8
8
|
import { canonicalizeDeployClaimV1, createDeployRequestId, encodeDeployClaimV1 } from "./deploy-contract.mjs";
|
|
9
|
-
import {
|
|
10
|
-
DEPLOY_LOCKFILE_CANDIDATES,
|
|
11
|
-
UNSUPPORTED_DEPLOY_LOCKFILES,
|
|
12
|
-
} from "./deploy-lockfile-selection.mjs";
|
|
9
|
+
import { DEPLOY_LOCKFILE_CANDIDATES, UNSUPPORTED_DEPLOY_LOCKFILES } from "./deploy-lockfile-selection.mjs";
|
|
13
10
|
|
|
14
11
|
const PACKAGE_MANIFEST = "package.json";
|
|
15
12
|
const MIGRATION_MANIFEST = ".gencow/migration-bundle-manifest.json";
|
|
@@ -57,6 +54,22 @@ function findAuthSchemaFiles(allFiles, pattern) {
|
|
|
57
54
|
return [...allFiles].filter((path) => pattern.test(path)).sort();
|
|
58
55
|
}
|
|
59
56
|
|
|
57
|
+
function assertDeployDependencyLock(packageBytes, lockfileBytes) {
|
|
58
|
+
const admission = inspectPnpmDependencyLock({
|
|
59
|
+
packageJsonText: packageBytes.toString("utf8"),
|
|
60
|
+
pnpmLockText: lockfileBytes?.toString("utf8"),
|
|
61
|
+
});
|
|
62
|
+
if (admission.ok) return;
|
|
63
|
+
const action =
|
|
64
|
+
admission.code === "APP_DEPENDENCY_LOCK_MISSING"
|
|
65
|
+
? "pnpm-lock.yaml is required. Run pnpm install and retry."
|
|
66
|
+
: "package.json and pnpm-lock.yaml do not describe the same standalone app. Run pnpm install and retry.";
|
|
67
|
+
const error = new Error(`${admission.code}: ${action}`);
|
|
68
|
+
error.code = admission.code;
|
|
69
|
+
error.files = [...admission.files];
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
async function selectedArchiveFiles(bundle) {
|
|
61
74
|
const selected = new Map();
|
|
62
75
|
const allFiles = new Set();
|
|
@@ -99,7 +112,9 @@ export async function buildDeployClaimFromArchive({
|
|
|
99
112
|
const lockfiles = [...LOCKFILES].filter(([path]) => selected.has(path));
|
|
100
113
|
const unsupportedLockfiles = UNSUPPORTED_DEPLOY_LOCKFILES.filter((path) => selected.has(path));
|
|
101
114
|
if (unsupportedLockfiles.length > 0) {
|
|
102
|
-
throw new Error(
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Deploy dependency lockfile is ambiguous or unsupported: ${unsupportedLockfiles.join(", ")}`,
|
|
117
|
+
);
|
|
103
118
|
}
|
|
104
119
|
if (lockfiles.length > 1) throw new Error("Deploy dependency lockfile is ambiguous");
|
|
105
120
|
const projectAuth = findAuthSchemaFiles(allFiles, CANONICAL_PROJECT_AUTH_PATTERN);
|
|
@@ -120,6 +135,9 @@ export async function buildDeployClaimFromArchive({
|
|
|
120
135
|
: { mode: "builtin", path: null };
|
|
121
136
|
const packageBytes = selected.get(PACKAGE_MANIFEST);
|
|
122
137
|
const lockfile = lockfiles[0];
|
|
138
|
+
if (declaredRuntime === "server" && packageBytes) {
|
|
139
|
+
assertDeployDependencyLock(packageBytes, lockfile ? selected.get(lockfile[0]) : null);
|
|
140
|
+
}
|
|
123
141
|
const migrationBytes = selected.get(MIGRATION_MANIFEST);
|
|
124
142
|
const claim = canonicalizeDeployClaimV1({
|
|
125
143
|
protocolVersion: 1,
|
|
@@ -209,6 +209,11 @@ export function createDeployPackageRuntime({
|
|
|
209
209
|
resolvePathImpl,
|
|
210
210
|
})
|
|
211
211
|
) {
|
|
212
|
+
const selection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
|
|
213
|
+
if (selection.selectedFile && migrationProjectConfig?.schema) {
|
|
214
|
+
throw new Error("Configured schema source is missing from the project");
|
|
215
|
+
}
|
|
216
|
+
migrationProjectConfig = null;
|
|
212
217
|
logImpl(`${DIM} ok No schema source — migration generation not required${RESET}`);
|
|
213
218
|
return SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION;
|
|
214
219
|
}
|
|
@@ -381,12 +386,14 @@ export function createDeployPackageRuntime({
|
|
|
381
386
|
return null;
|
|
382
387
|
}
|
|
383
388
|
|
|
384
|
-
const lockfileSelection = assertCanonicalDeployLockfile(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
const lockfileSelection = assertCanonicalDeployLockfile(
|
|
390
|
+
detectDeployLockfile({
|
|
391
|
+
cwd,
|
|
392
|
+
packageManager: projectPkg.packageManager,
|
|
393
|
+
existsSyncImpl,
|
|
394
|
+
resolvePathImpl,
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
390
397
|
const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
|
|
391
398
|
if (lockfileWarning) warnImpl(lockfileWarning);
|
|
392
399
|
const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
|
|
@@ -672,7 +679,9 @@ export function createDeployPackageRuntime({
|
|
|
672
679
|
errorImpl(lines[0]);
|
|
673
680
|
lines.slice(1).forEach((line) => logImpl(line));
|
|
674
681
|
} else {
|
|
675
|
-
const lines = renderDeployFailureDiagnosticLines(errData, deployRes.statusText
|
|
682
|
+
const lines = renderDeployFailureDiagnosticLines(errData, deployRes.statusText, {
|
|
683
|
+
expectedAppName: targetAppId,
|
|
684
|
+
});
|
|
676
685
|
errorImpl(lines[0]);
|
|
677
686
|
lines
|
|
678
687
|
.slice(1)
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./app-diagnostics.mjs";
|
|
15
15
|
import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
|
|
16
16
|
import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
|
|
17
|
+
import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
|
|
17
18
|
import { hasProjectDatabaseSchemaSource } from "./project-migration-manifest.mjs";
|
|
18
19
|
import {
|
|
19
20
|
extractActionableMigrationDiagnostic,
|
|
@@ -295,11 +296,25 @@ export function createDevCloudCommand(deps) {
|
|
|
295
296
|
...protocolHeaders,
|
|
296
297
|
},
|
|
297
298
|
});
|
|
298
|
-
|
|
299
|
+
// A successful POST only means the control plane accepted the bundle.
|
|
300
|
+
// Backend deployment, migration, and activation run asynchronously; do
|
|
301
|
+
// not tell `gencow dev` users that deployment completed until the
|
|
302
|
+
// returned operation reaches a terminal state.
|
|
303
|
+
const completedRes =
|
|
304
|
+
res.status === 202
|
|
305
|
+
? await pollAcceptedDeploymentOperation({
|
|
306
|
+
initialResponse: res,
|
|
307
|
+
creds,
|
|
308
|
+
expectedAppName: appName,
|
|
309
|
+
platformFetch: deps.platformFetchImpl,
|
|
310
|
+
setTimeoutImpl: deps.setTimeoutImpl,
|
|
311
|
+
})
|
|
312
|
+
: res;
|
|
313
|
+
const data = await completedRes.json();
|
|
299
314
|
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
|
300
315
|
const sizeKB = (bundle.length / 1024).toFixed(1);
|
|
301
316
|
|
|
302
|
-
if (!
|
|
317
|
+
if (!completedRes.ok) {
|
|
303
318
|
const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
304
319
|
deps.logImpl(`${deps.DIM}${ts2}${deps.RESET} ${deps.RED}[deploy]${deps.RESET} ✗ Deploy failed!`);
|
|
305
320
|
const errMsg = data.error || "Unknown error";
|