gencow 0.1.135 → 0.1.136
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/bin/gencow.mjs +1 -3
- package/core/index.js +17 -0
- package/lib/__tests__/ai-template-smoke.test.ts +18 -2
- package/lib/__tests__/cli-project-runtime.test.mjs +15 -1
- package/lib/__tests__/codegen-command-output-dir.test.mjs +46 -0
- package/lib/__tests__/codegen-command.test.mjs +45 -21
- package/lib/__tests__/dev-local-command.test.mjs +3 -1
- package/lib/__tests__/init-command.test.mjs +5 -0
- package/lib/__tests__/name-casing.test.mjs +26 -0
- package/lib/__tests__/test-script-regression.test.ts +92 -0
- package/lib/api-codegen.mjs +31 -4
- package/lib/cli-dev-runtime.mjs +103 -127
- package/lib/cli-project-runtime.mjs +20 -2
- package/lib/codegen/index.mjs +1505 -0
- package/lib/codegen-command.mjs +63 -100
- package/lib/dev-cloud-command.mjs +1 -1
- package/lib/dev-local-command.mjs +9 -4
- package/lib/init-command.mjs +7 -1
- package/lib/name-casing.d.ts +2 -0
- package/lib/name-casing.mjs +17 -0
- package/package.json +5 -6
- package/scripts/bundle-codegen.mjs +39 -0
- package/scripts/pre-publish-check.mjs +15 -1
- package/server/index.js +4 -0
- package/server/index.js.map +2 -2
- package/dashboard/apple-touch-icon.png +0 -0
- package/dashboard/assets/index-C1IxULeS.css +0 -1
- package/dashboard/assets/index-D0ZYKx4B.js +0 -378
- package/dashboard/favicon-16.png +0 -0
- package/dashboard/favicon-192.png +0 -0
- package/dashboard/favicon-32.png +0 -0
- package/dashboard/favicon-512.png +0 -0
- package/dashboard/favicon.ico +0 -0
- package/dashboard/favicon.svg +0 -1
- package/dashboard/file.svg +0 -1
- package/dashboard/globe.svg +0 -1
- package/dashboard/index.html +0 -29
- package/dashboard/next.svg +0 -1
- package/dashboard/vercel.svg +0 -1
- package/dashboard/window.svg +0 -1
package/bin/gencow.mjs
CHANGED
|
@@ -75,9 +75,7 @@ import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "../lib/platf
|
|
|
75
75
|
|
|
76
76
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
77
77
|
const _drizzleKitCmd = (subcmd) => buildDrizzleKitCommand(subcmd);
|
|
78
|
-
const generateApiTs = createApiCodegenRuntime({
|
|
79
|
-
findServerRootImpl: findServerRoot,
|
|
80
|
-
});
|
|
78
|
+
const generateApiTs = createApiCodegenRuntime({});
|
|
81
79
|
const runAddCommand = createAddCommand({
|
|
82
80
|
loadConfig,
|
|
83
81
|
updateComponentReadmeImpl: updateComponentReadme,
|
package/core/index.js
CHANGED
|
@@ -2683,6 +2683,13 @@ var _ownerRlsTables = [];
|
|
|
2683
2683
|
function getOwnerRlsTables() {
|
|
2684
2684
|
return _ownerRlsTables;
|
|
2685
2685
|
}
|
|
2686
|
+
if (!globalThis.__gencow_crudCodegenRegistry) {
|
|
2687
|
+
globalThis.__gencow_crudCodegenRegistry = /* @__PURE__ */ new Map();
|
|
2688
|
+
}
|
|
2689
|
+
var crudCodegenRegistry = globalThis.__gencow_crudCodegenRegistry;
|
|
2690
|
+
function getRegisteredCrudCodegenMeta() {
|
|
2691
|
+
return Array.from(crudCodegenRegistry.values());
|
|
2692
|
+
}
|
|
2686
2693
|
function detectIdType(column) {
|
|
2687
2694
|
const colType = column.dataType;
|
|
2688
2695
|
if (colType === "string") return v.string();
|
|
@@ -2872,6 +2879,15 @@ function crud(table, options) {
|
|
|
2872
2879
|
});
|
|
2873
2880
|
}
|
|
2874
2881
|
const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
|
|
2882
|
+
const enabledMethodsList = Array.from(enabledMethods);
|
|
2883
|
+
crudCodegenRegistry.set(prefix, {
|
|
2884
|
+
tableName,
|
|
2885
|
+
prefix,
|
|
2886
|
+
methods: enabledMethodsList,
|
|
2887
|
+
allowedFilters: (options?.allowedFilters ?? []).map((field) => String(field)),
|
|
2888
|
+
searchFields: (options?.searchFields ?? []).map((field) => String(field)),
|
|
2889
|
+
isPublic
|
|
2890
|
+
});
|
|
2875
2891
|
const listDef = !enabledMethods.has("list") ? void 0 : query(`${prefix}.list`, {
|
|
2876
2892
|
public: isPublic,
|
|
2877
2893
|
args: {
|
|
@@ -3052,6 +3068,7 @@ export {
|
|
|
3052
3068
|
getOwnerRlsTables,
|
|
3053
3069
|
getQueryDef,
|
|
3054
3070
|
getQueryHandler,
|
|
3071
|
+
getRegisteredCrudCodegenMeta,
|
|
3055
3072
|
getRegisteredHttpActions,
|
|
3056
3073
|
getRegisteredMutations,
|
|
3057
3074
|
getRegisteredQueries,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import { join, resolve } from "path";
|
|
4
5
|
import { tmpdir } from "os";
|
|
5
6
|
|
|
@@ -9,17 +10,32 @@ const syncedTemplatePaths = [
|
|
|
9
10
|
join(repoRoot, "packages/cli/templates/fullstack/ai.ts"),
|
|
10
11
|
join(repoRoot, "packages/cli/templates/ai-chat/ai.ts"),
|
|
11
12
|
];
|
|
13
|
+
const fixtureWorkspacePath = join(repoRoot, "apps/test-ai-template-fixture");
|
|
12
14
|
const fixtureNodeModules = join(repoRoot, "apps/test-ai-template-fixture/node_modules");
|
|
15
|
+
const fixtureRequire = createRequire(join(fixtureWorkspacePath, "package.json"));
|
|
13
16
|
|
|
14
17
|
function readTemplate(path: string): string {
|
|
15
18
|
return readFileSync(path, "utf8");
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
function requireFixtureNodeModules(): string {
|
|
22
|
+
const requiredPackages = ["typescript/bin/tsc", "ai/package.json", "@ai-sdk/openai/package.json", "zod/package.json"];
|
|
23
|
+
for (const packagePath of requiredPackages) {
|
|
24
|
+
try {
|
|
25
|
+
fixtureRequire.resolve(packagePath);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"AI template smoke test requires the fixture workspace dependencies from a normal repo install. " +
|
|
29
|
+
"Run `pnpm install` at the repo root first.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
if (existsSync(fixtureNodeModules)) return fixtureNodeModules;
|
|
35
|
+
|
|
20
36
|
throw new Error(
|
|
21
|
-
"AI template smoke test requires
|
|
22
|
-
"Run `pnpm install
|
|
37
|
+
"AI template smoke test requires the fixture workspace links from a normal repo install. " +
|
|
38
|
+
"Run `pnpm install` at the repo root first.",
|
|
23
39
|
);
|
|
24
40
|
}
|
|
25
41
|
|
|
@@ -22,6 +22,7 @@ describe("loadConfig()", () => {
|
|
|
22
22
|
).toEqual({
|
|
23
23
|
functionsDir: "./gencow",
|
|
24
24
|
schema: "./gencow/schema.ts",
|
|
25
|
+
codegenOutDir: "./src/gencow",
|
|
25
26
|
storage: "./.gencow/uploads",
|
|
26
27
|
db: { url: "./.gencow/data" },
|
|
27
28
|
port: 5456,
|
|
@@ -33,12 +34,13 @@ describe("loadConfig()", () => {
|
|
|
33
34
|
const root = mkdtempSync(resolve(tmpdir(), "gencow-cli-config-"));
|
|
34
35
|
writeFileSync(
|
|
35
36
|
resolve(root, "gencow.config.ts"),
|
|
36
|
-
`export default { functionsDir: "./api", schema: "./api/schema.ts", storage: "./uploads", db: { url: "./data" }, port: 6001 };`,
|
|
37
|
+
`export default { functionsDir: "./api", schema: "./api/schema.ts", codegenOutDir: "./src/gencow", storage: "./uploads", db: { url: "./data" }, port: 6001 };`,
|
|
37
38
|
);
|
|
38
39
|
|
|
39
40
|
expect(loadConfig({ cwd: root })).toEqual({
|
|
40
41
|
functionsDir: "./api",
|
|
41
42
|
schema: "./api/schema.ts",
|
|
43
|
+
codegenOutDir: "./src/gencow",
|
|
42
44
|
storage: "./uploads",
|
|
43
45
|
db: { url: "./data" },
|
|
44
46
|
port: 6001,
|
|
@@ -46,6 +48,18 @@ describe("loadConfig()", () => {
|
|
|
46
48
|
|
|
47
49
|
rmSync(root, { recursive: true, force: true });
|
|
48
50
|
});
|
|
51
|
+
|
|
52
|
+
it("parses schema arrays from gencow.config.ts", () => {
|
|
53
|
+
const root = mkdtempSync(resolve(tmpdir(), "gencow-cli-config-schema-array-"));
|
|
54
|
+
writeFileSync(
|
|
55
|
+
resolve(root, "gencow.config.ts"),
|
|
56
|
+
`export default { schema: ["./src/custom-db.ts", "./src/auth-model.ts"] };`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(loadConfig({ cwd: root }).schema).toEqual(["./src/custom-db.ts", "./src/auth-model.ts"]);
|
|
60
|
+
|
|
61
|
+
rmSync(root, { recursive: true, force: true });
|
|
62
|
+
});
|
|
49
63
|
});
|
|
50
64
|
|
|
51
65
|
describe("findServerRoot()", () => {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { resolveCodegenPublishDir } from "../codegen-command.mjs";
|
|
6
|
+
|
|
7
|
+
describe("resolveCodegenPublishDir()", () => {
|
|
8
|
+
it("prefers codegenOutDir from config when provided", () => {
|
|
9
|
+
const root = mkdtempSync(resolve(tmpdir(), "gencow-codegen-outdir-"));
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(resolve(root, "src"), { recursive: true });
|
|
12
|
+
const out = resolveCodegenPublishDir(root, {
|
|
13
|
+
functionsDir: "./gencow",
|
|
14
|
+
codegenOutDir: "./custom/generated",
|
|
15
|
+
});
|
|
16
|
+
expect(out).toBe(resolve(root, "custom/generated"));
|
|
17
|
+
} finally {
|
|
18
|
+
rmSync(root, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("falls back to src/gencow when src exists", () => {
|
|
23
|
+
const root = mkdtempSync(resolve(tmpdir(), "gencow-codegen-outdir-"));
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(resolve(root, "src"), { recursive: true });
|
|
26
|
+
const out = resolveCodegenPublishDir(root, {
|
|
27
|
+
functionsDir: "./gencow",
|
|
28
|
+
});
|
|
29
|
+
expect(out).toBe(resolve(root, "src/gencow"));
|
|
30
|
+
} finally {
|
|
31
|
+
rmSync(root, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to functionsDir when src does not exist", () => {
|
|
36
|
+
const root = mkdtempSync(resolve(tmpdir(), "gencow-codegen-outdir-"));
|
|
37
|
+
try {
|
|
38
|
+
const out = resolveCodegenPublishDir(root, {
|
|
39
|
+
functionsDir: "./gencow",
|
|
40
|
+
});
|
|
41
|
+
expect(out).toBe(resolve(root, "gencow"));
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(root, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,28 +1,52 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCodegenCommand } from "../codegen-command.mjs";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
describe("createCodegenCommand()", () => {
|
|
5
|
+
it("passes multi-schema config to runCodegen from CLI boundary", async () => {
|
|
6
|
+
let capturedInput = null;
|
|
7
|
+
const logs = [];
|
|
8
|
+
const command = createCodegenCommand({
|
|
9
|
+
BOLD: "",
|
|
10
|
+
CYAN: "",
|
|
11
|
+
DIM: "",
|
|
12
|
+
RED: "",
|
|
13
|
+
RESET: "",
|
|
14
|
+
YELLOW: "",
|
|
15
|
+
loadConfig: () => ({
|
|
16
|
+
functionsDir: "./src/gencow",
|
|
17
|
+
schema: ["./src/custom-db.ts", "./src/auth-model.ts"],
|
|
18
|
+
codegenOutDir: "./src/gencow/gen",
|
|
19
|
+
}),
|
|
20
|
+
cwdImpl: () => "/tmp/project",
|
|
21
|
+
resolvePathImpl: (cwd, p) => `${cwd}/${p.replace(/^\.\//, "")}`,
|
|
22
|
+
logImpl: (line) => logs.push(String(line)),
|
|
23
|
+
infoImpl: (line) => logs.push(String(line)),
|
|
24
|
+
successImpl: () => {},
|
|
25
|
+
errorImpl: (line) => logs.push(String(line)),
|
|
26
|
+
exitImpl: (code) => {
|
|
27
|
+
throw new Error(`Unexpected exit: ${code}`);
|
|
28
|
+
},
|
|
29
|
+
loadCodegenBundleImpl: async () => ({
|
|
30
|
+
runCodegen: async (input) => {
|
|
31
|
+
capturedInput = input;
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
diagnostics: [],
|
|
35
|
+
artifacts: [{ path: "manifest.json" }, { path: "api.ts" }],
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
9
40
|
|
|
10
|
-
|
|
11
|
-
expect(resolveCodegenOutdir("/repo/demo", ["--outdir", "web/gen"])).toBe("/repo/demo/web/gen");
|
|
12
|
-
expect(resolveCodegenOutdir("/repo/demo", ["-o", "app/api"])).toBe("/repo/demo/app/api");
|
|
13
|
-
});
|
|
14
|
-
});
|
|
41
|
+
await command();
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
43
|
+
expect(capturedInput).toEqual({
|
|
44
|
+
projectRoot: "/tmp/project",
|
|
45
|
+
functionsDir: "./src/gencow",
|
|
46
|
+
schema: ["./src/custom-db.ts", "./src/auth-model.ts"],
|
|
47
|
+
outDir: "/tmp/project/.gencow/codegen",
|
|
48
|
+
finalOutDir: "/tmp/project/src/gencow/gen",
|
|
21
49
|
});
|
|
22
|
-
|
|
23
|
-
expect(out).toContain('import { defineQuery, defineMutation } from "@gencow/react";');
|
|
24
|
-
expect(out).toContain('list: defineQuery("tasks.list")');
|
|
25
|
-
expect(out).toContain('create: defineMutation("tasks.create")');
|
|
26
|
-
expect(out).toContain('me: defineQuery("users.me")');
|
|
50
|
+
expect(logs.join("\n")).toContain("Output:");
|
|
27
51
|
});
|
|
28
52
|
});
|
|
@@ -25,9 +25,11 @@ describe("buildKillPortsCommand()", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe("isGeneratedCodegenFile()", () => {
|
|
28
|
-
it("filters generated
|
|
28
|
+
it("filters generated client artifact basenames", () => {
|
|
29
29
|
expect(isGeneratedCodegenFile("gencow/api.ts")).toBe(true);
|
|
30
30
|
expect(isGeneratedCodegenFile("nested/README.md")).toBe(true);
|
|
31
|
+
expect(isGeneratedCodegenFile("gencow/db.d.ts")).toBe(true);
|
|
32
|
+
expect(isGeneratedCodegenFile("gencow/operations.d.ts")).toBe(true);
|
|
31
33
|
expect(isGeneratedCodegenFile("schema.ts")).toBe(false);
|
|
32
34
|
});
|
|
33
35
|
});
|
|
@@ -58,6 +58,11 @@ describe("mergeGitignoreContent()", () => {
|
|
|
58
58
|
const merged = mergeGitignoreContent("dist/\nnode_modules/\n");
|
|
59
59
|
expect(merged).toContain("# Gencow");
|
|
60
60
|
expect(merged).toContain(".gencow/");
|
|
61
|
+
expect(merged).toContain("src/gencow/api.ts");
|
|
62
|
+
expect(merged).toContain("src/gencow/db.d.ts");
|
|
63
|
+
expect(merged).toContain("src/gencow/operations.d.ts");
|
|
64
|
+
expect(merged).toContain("src/gencow/zod.ts");
|
|
65
|
+
expect(merged).toContain("gencow/zod.ts");
|
|
61
66
|
expect(merged.match(/node_modules\//g)?.length).toBe(1);
|
|
62
67
|
});
|
|
63
68
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { toCamelCase, toPascalCase } from "../name-casing.mjs";
|
|
3
|
+
|
|
4
|
+
describe("toPascalCase", () => {
|
|
5
|
+
it("splits on non-alphanumeric runs and capitalizes segments", () => {
|
|
6
|
+
expect(toPascalCase("tasks")).toBe("Tasks");
|
|
7
|
+
expect(toPascalCase("my_tasks")).toBe("MyTasks");
|
|
8
|
+
expect(toPascalCase("my-tasks.list")).toBe("MyTasksList");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("coerces non-strings like api-codegen callers", () => {
|
|
12
|
+
expect(toPascalCase(12)).toBe("12");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("toCamelCase", () => {
|
|
17
|
+
it("lowercases the first character of PascalCase", () => {
|
|
18
|
+
expect(toCamelCase("tasks")).toBe("tasks");
|
|
19
|
+
expect(toCamelCase("my_tasks")).toBe("myTasks");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns empty string for empty Pascal base", () => {
|
|
23
|
+
expect(toCamelCase("")).toBe("");
|
|
24
|
+
expect(toCamelCase("___")).toBe("");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
|
|
5
|
+
const repoRoot = resolve(import.meta.dir, "../../../..");
|
|
6
|
+
const helperPath = join(repoRoot, "scripts/lib/test-helpers.sh");
|
|
7
|
+
const aiTemplateSmokePath = join(repoRoot, "packages/cli/lib/__tests__/ai-template-smoke.test.ts");
|
|
8
|
+
|
|
9
|
+
const coreShellScripts = [
|
|
10
|
+
"scripts/test-server.sh",
|
|
11
|
+
"scripts/test-auth-flow.sh",
|
|
12
|
+
"scripts/test-security.sh",
|
|
13
|
+
"scripts/test-realtime.sh",
|
|
14
|
+
"scripts/test-cron-scheduler.sh",
|
|
15
|
+
"scripts/test-env.sh",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
function readRepoFile(path: string): string {
|
|
19
|
+
return readFileSync(path, "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("shell test helper regression", () => {
|
|
23
|
+
it("keeps core shell suites on the shared Bun-based helper path", () => {
|
|
24
|
+
const helperSource = readRepoFile(helperPath);
|
|
25
|
+
expect(helperSource).toContain("json_get()");
|
|
26
|
+
expect(helperSource).toContain("run_with_timeout()");
|
|
27
|
+
expect(helperSource).toContain("make_test_email()");
|
|
28
|
+
|
|
29
|
+
for (const relativePath of coreShellScripts) {
|
|
30
|
+
const source = readRepoFile(join(repoRoot, relativePath));
|
|
31
|
+
expect(source).toContain('source "${SCRIPT_DIR}/lib/test-helpers.sh"');
|
|
32
|
+
expect(source).not.toContain("python3 -c");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses per-run identities for remote auth fixtures", () => {
|
|
37
|
+
const serverSource = readRepoFile(join(repoRoot, "scripts/test-server.sh"));
|
|
38
|
+
const securitySource = readRepoFile(join(repoRoot, "scripts/test-security.sh"));
|
|
39
|
+
const realtimeSource = readRepoFile(join(repoRoot, "scripts/test-realtime.sh"));
|
|
40
|
+
|
|
41
|
+
expect(serverSource).toContain('make_test_email "server-e2e"');
|
|
42
|
+
expect(serverSource).not.toContain("test@example.com");
|
|
43
|
+
|
|
44
|
+
expect(securitySource).toContain('make_test_email "security-e2e"');
|
|
45
|
+
expect(securitySource).not.toContain("sectest@example.com");
|
|
46
|
+
|
|
47
|
+
expect(realtimeSource).toContain('make_test_email "realtime-e2e"');
|
|
48
|
+
expect(realtimeSource).not.toContain("realtime@example.com");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps realtime smoke portable across timeout implementations", () => {
|
|
52
|
+
const realtimeSource = readRepoFile(join(repoRoot, "scripts/test-realtime.sh"));
|
|
53
|
+
|
|
54
|
+
expect(realtimeSource).toContain("run_with_timeout 3 websocat");
|
|
55
|
+
expect(realtimeSource).not.toMatch(/\n\s*timeout 3 websocat/);
|
|
56
|
+
expect(realtimeSource).toContain("portable JSON parsing + timeout fallback");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("treats admin access checks as policy-aware instead of hard-coded public 200s", () => {
|
|
60
|
+
const securitySource = readRepoFile(join(repoRoot, "scripts/test-security.sh"));
|
|
61
|
+
|
|
62
|
+
expect(securitySource).toContain('EXPECT_ADMIN_ACCESS="${EXPECT_ADMIN_ACCESS:-auto}"');
|
|
63
|
+
expect(securitySource).toContain("assert_admin_access_policy");
|
|
64
|
+
expect(securitySource).not.toContain('assert_status "Admin status → 200" "200" "$ADMIN_STATUS"');
|
|
65
|
+
expect(securitySource).not.toContain('assert_status "Admin tables → 200" "200" "$ADMIN_TABLES"');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("AI template smoke fixture regression", () => {
|
|
70
|
+
it("anchors fixture dependency messaging to a normal repo install", () => {
|
|
71
|
+
const source = readRepoFile(aiTemplateSmokePath);
|
|
72
|
+
|
|
73
|
+
expect(source).toContain('createRequire(join(fixtureWorkspacePath, "package.json"))');
|
|
74
|
+
expect(source).toContain("Run `pnpm install` at the repo root first.");
|
|
75
|
+
expect(source).not.toContain("pnpm install --filter ./apps/test-ai-template-fixture");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("cowbox DEV test path regression", () => {
|
|
80
|
+
it("exposes a dedicated DEV Linux test entrypoint for cowbox", () => {
|
|
81
|
+
const packageJson = readRepoFile(join(repoRoot, "package.json"));
|
|
82
|
+
const scriptSource = readRepoFile(join(repoRoot, "scripts/test-cowbox-dev.sh"));
|
|
83
|
+
|
|
84
|
+
expect(packageJson).toContain('"test:cowbox:dev": "./scripts/test-cowbox-dev.sh"');
|
|
85
|
+
expect(scriptSource).toContain('DEV_HOST="${GENCOW_DEV_HOST:-root@100.114.201.127}"');
|
|
86
|
+
expect(scriptSource).toContain("cargo test -- --nocapture");
|
|
87
|
+
expect(scriptSource).toContain("./target/release/cowbox check");
|
|
88
|
+
expect(scriptSource).toContain("./target/release/cowbox run");
|
|
89
|
+
expect(scriptSource).toContain("allowed write smoke");
|
|
90
|
+
expect(scriptSource).toContain("denied write smoke");
|
|
91
|
+
});
|
|
92
|
+
});
|
package/lib/api-codegen.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toPascalCase } from "./name-casing.mjs";
|
|
2
|
+
|
|
1
3
|
export const CORE_GENERATED_NAMESPACES = new Set(["workflows"]);
|
|
2
4
|
|
|
3
5
|
export function buildApiObject({ queries = [], mutations = [] }) {
|
|
@@ -39,13 +41,18 @@ export function buildGeneratedApiTs({
|
|
|
39
41
|
namespaceImportPaths = {},
|
|
40
42
|
singleFileImportPath = "./index.ts",
|
|
41
43
|
coreGeneratedNamespaces = CORE_GENERATED_NAMESPACES,
|
|
44
|
+
useCrudTypes = false,
|
|
45
|
+
crudTypeImportPath = "./crud.d.ts",
|
|
46
|
+
crudTypeNamespaces = [],
|
|
42
47
|
}) {
|
|
43
|
-
const hasNamespaceFiles = Object.keys(namespaceImportPaths).length > 0;
|
|
48
|
+
const hasNamespaceFiles = !useCrudTypes && Object.keys(namespaceImportPaths).length > 0;
|
|
44
49
|
|
|
45
50
|
let out = `/**\n * Generated by Gencow\n * Do not edit this file manually.\n */\n`;
|
|
46
51
|
out += `import { defineQuery, defineMutation } from "@gencow/react";\n\n`;
|
|
47
52
|
|
|
48
|
-
if (
|
|
53
|
+
if (useCrudTypes) {
|
|
54
|
+
out += `import type * as Crud from "${crudTypeImportPath}";\n`;
|
|
55
|
+
} else if (hasNamespaceFiles) {
|
|
49
56
|
for (const [namespace, importPath] of Object.entries(namespaceImportPaths)) {
|
|
50
57
|
out += `import type * as ${namespace} from "${importPath}";\n`;
|
|
51
58
|
}
|
|
@@ -55,6 +62,8 @@ export function buildGeneratedApiTs({
|
|
|
55
62
|
|
|
56
63
|
out += `\nexport const api = {\n`;
|
|
57
64
|
for (const [namespace, fns] of Object.entries(apiObj)) {
|
|
65
|
+
const namespaceBase = toPascalCase(namespace);
|
|
66
|
+
const hasCrudTypesForNamespace = useCrudTypes && crudTypeNamespaces.includes(namespace);
|
|
58
67
|
const typedReference = canUseTypedReference({
|
|
59
68
|
namespace,
|
|
60
69
|
hasNamespaceFiles,
|
|
@@ -65,7 +74,15 @@ export function buildGeneratedApiTs({
|
|
|
65
74
|
out += ` ${namespace}: {\n`;
|
|
66
75
|
for (const queryName of fns.queries) {
|
|
67
76
|
const tsExport = toTsExportName(queryName);
|
|
68
|
-
if (
|
|
77
|
+
if (hasCrudTypesForNamespace) {
|
|
78
|
+
if (queryName === "list") {
|
|
79
|
+
out += ` ${queryName}: defineQuery<{ _args?: Crud.${namespaceBase}ListInput; _return?: Crud.${namespaceBase}ListOutput }>("${namespace}.${queryName}"),\n`;
|
|
80
|
+
} else if (queryName === "get") {
|
|
81
|
+
out += ` ${queryName}: defineQuery<{ _args?: Crud.${namespaceBase}GetInput; _return?: Crud.${namespaceBase}Select }>("${namespace}.${queryName}"),\n`;
|
|
82
|
+
} else {
|
|
83
|
+
out += ` ${queryName}: defineQuery("${namespace}.${queryName}"),\n`;
|
|
84
|
+
}
|
|
85
|
+
} else if (typedReference && hasNamespaceFiles) {
|
|
69
86
|
out += ` ${queryName}: defineQuery<typeof ${namespace}.${tsExport}>("${namespace}.${queryName}"),\n`;
|
|
70
87
|
} else if (typedReference) {
|
|
71
88
|
out += ` ${queryName}: defineQuery<typeof _all.${tsExport}>("${namespace}.${queryName}"),\n`;
|
|
@@ -75,7 +92,17 @@ export function buildGeneratedApiTs({
|
|
|
75
92
|
}
|
|
76
93
|
for (const mutationName of fns.mutations) {
|
|
77
94
|
const tsExport = toTsExportName(mutationName);
|
|
78
|
-
if (
|
|
95
|
+
if (hasCrudTypesForNamespace) {
|
|
96
|
+
if (mutationName === "create") {
|
|
97
|
+
out += ` ${mutationName}: defineMutation<{ _args?: Crud.${namespaceBase}CreateInput; _return?: Crud.${namespaceBase}Select }>("${namespace}.${mutationName}"),\n`;
|
|
98
|
+
} else if (mutationName === "update") {
|
|
99
|
+
out += ` ${mutationName}: defineMutation<{ _args?: Crud.${namespaceBase}UpdateInput; _return?: Crud.${namespaceBase}Select }>("${namespace}.${mutationName}"),\n`;
|
|
100
|
+
} else if (mutationName === "remove") {
|
|
101
|
+
out += ` ${mutationName}: defineMutation<{ _args?: Crud.${namespaceBase}RemoveInput; _return?: unknown }>("${namespace}.${mutationName}"),\n`;
|
|
102
|
+
} else {
|
|
103
|
+
out += ` ${mutationName}: defineMutation("${namespace}.${mutationName}"),\n`;
|
|
104
|
+
}
|
|
105
|
+
} else if (typedReference && hasNamespaceFiles) {
|
|
79
106
|
out += ` ${mutationName}: defineMutation<typeof ${namespace}.${tsExport}>("${namespace}.${mutationName}"),\n`;
|
|
80
107
|
} else if (typedReference) {
|
|
81
108
|
out += ` ${mutationName}: defineMutation<typeof _all.${tsExport}>("${namespace}.${mutationName}"),\n`;
|