@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.10

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.
@@ -0,0 +1,189 @@
1
+ import { hostOf } from "../../catalog/host-entry.interface.js";
2
+ import { lowestVersionOf } from "../../catalog/range.reader.js";
3
+ import { EXPOSURE_DOCS } from "../docs.links.js";
4
+ import { SERVER_ENTRY } from "../next-server.entry.js";
5
+ import { SCAFFOLD_API, } from "../template.registry.js";
6
+ import { CLIENT_OUTPUT, discoveryImport, PROVIDERS, prepare, prismaConfig, resourcesTreeBeside, SCHEMA, schema, specifier, } from "./prisma7-orm.templates.js";
7
+ const NEXT = hostOf("next");
8
+ const CONFIG_FILE = `${NEXT.layout.config}.ts`;
9
+ const ADAPTER_FILE = `${NEXT.layout.adapter}.ts`;
10
+ const SERVER_FILE = SERVER_ENTRY;
11
+ const CLIENT_MODULE = "src/prisma.ts";
12
+ const JS = "";
13
+ function prismaClientModule(input, artifact) {
14
+ const provider = PROVIDERS[input.provider];
15
+ const driver = input.entry.drivers[input.provider];
16
+ return [
17
+ `import { ${provider.driverClass} } from "${driver}";`,
18
+ `import { PrismaClient } from "${specifier(CLIENT_MODULE, `${CLIENT_OUTPUT}/client`, JS)}";`,
19
+ `import { discovery } from "${discoveryImport(CLIENT_MODULE, artifact, JS)}";`,
20
+ "",
21
+ "const globalForPrisma = globalThis as typeof globalThis & {",
22
+ " prisma?: { readonly client: PrismaClient; readonly schema: string };",
23
+ "};",
24
+ "",
25
+ "/**",
26
+ " * The app's one PrismaClient, made on first use: importing this module connects",
27
+ " * to nothing. It is kept on globalThis, so a reload in `next dev` reuses it",
28
+ " * instead of opening another connection, for as long as the generated schema",
29
+ " * is the same. After `aventara:prepare` regenerates the client, the next call",
30
+ " * makes a new one from the new generated class and disconnects the old one.",
31
+ " */",
32
+ "export function prisma(): PrismaClient {",
33
+ " const kept = globalForPrisma.prisma;",
34
+ " if (kept !== undefined && kept.schema === discovery.modelHash) {",
35
+ " return kept.client;",
36
+ " }",
37
+ " const url = process.env.DATABASE_URL;",
38
+ " if (!url) {",
39
+ " throw new Error(",
40
+ ' "DATABASE_URL is not set: put it in .env (Next.js loads it) or in the environment.",',
41
+ " );",
42
+ " }",
43
+ " const client = new PrismaClient({",
44
+ ` adapter: new ${provider.driverClass}(${provider.driverOptions}),`,
45
+ " });",
46
+ " globalForPrisma.prisma = { client, schema: discovery.modelHash };",
47
+ " kept?.client.$disconnect().catch(() => undefined);",
48
+ " return client;",
49
+ "}",
50
+ "",
51
+ ].join("\n");
52
+ }
53
+ function aventaraConfig(artifact) {
54
+ return [
55
+ 'import type { AvDiagnostic } from "@aventara/core";',
56
+ `import { aventaraResources } from "${specifier(CONFIG_FILE, resourcesTreeBeside(artifact), JS)}";`,
57
+ "",
58
+ "/**",
59
+ " * Aventara's configuration: where the API is mounted (`api`), and what you add",
60
+ " * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
61
+ " * `resources` tree).",
62
+ " * server.ts builds the framework from it and the adapter (adapter.ts).",
63
+ " * aventara-build, in the aventara:prepare script, runs this file to compile its",
64
+ " * Contracts and write the browser client, so keep its top level free of side",
65
+ " * effects: no connection, no request.",
66
+ " */",
67
+ "export const aventaraConfig = {",
68
+ " // The route files under src/app/api/[...aventara]/ serve it: keep the two in step.",
69
+ ` api: "${SCAFFOLD_API}",`,
70
+ " // Everything the adapter offers is exposed. List what to hide under",
71
+ " // `exemptions`, or set `exposed: false` and list what to expose instead.",
72
+ ` // See: ${EXPOSURE_DOCS}`,
73
+ " exposed: true,",
74
+ " // Every Resource you define with defineResource() in its own file under",
75
+ " // src/resources/ (named <name>.resource.ts), gathered by aventara-build in",
76
+ " // the aventara:prepare script: a new Resource file needs no line here. When",
77
+ " // a Resource gives a layer a block, that layer takes its tree once, e.g.",
78
+ " // client: { resources: aventaraResources.client },",
79
+ " resources: aventaraResources.root,",
80
+ " // When something fails inside the server, the caller gets a plain 500 and",
81
+ " // Aventara logs one line naming the request. This logs the error itself,",
82
+ " // with its stack, on the server's console.",
83
+ " diagnostics: (diagnostic: AvDiagnostic) => {",
84
+ " const { error } = diagnostic;",
85
+ " console.error(",
86
+ ` \`Aventara \${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
87
+ " (error instanceof Error && error.stack) || String(error),",
88
+ " );",
89
+ " },",
90
+ "} as const;",
91
+ "",
92
+ ].join("\n");
93
+ }
94
+ function aventaraAdapter(input, artifact) {
95
+ return [
96
+ `import { describePrismaAdapter } from "${input.entry.adapterPackage}";`,
97
+ `import { discovery } from "${discoveryImport(ADAPTER_FILE, artifact, JS)}";`,
98
+ "",
99
+ "/**",
100
+ " * The Prisma adapter's description: your schema's model, as the aventara:prepare",
101
+ " * script discovered it (`discovery`), and the database it runs on. aventara-build",
102
+ " * compiles the Contracts from it; server.ts builds the adapter from it with the",
103
+ " * Prisma client.",
104
+ " */",
105
+ "export default describePrismaAdapter({",
106
+ " discovery,",
107
+ ` provider: "${input.provider}",`,
108
+ ` driver: "${input.entry.drivers[input.provider]}",`,
109
+ "});",
110
+ "",
111
+ ].join("\n");
112
+ }
113
+ function serverEntry(input, artifact) {
114
+ const generated = artifact.replace(/[^/]+$/, "");
115
+ return [
116
+ 'import "server-only";',
117
+ 'import { Aventara } from "@aventara/next";',
118
+ `import { createPrismaAdapter } from "${input.entry.adapterPackage}";`,
119
+ `import { prisma } from "${specifier(SERVER_FILE, CLIENT_MODULE.replace(/\.ts$/, ""), JS)}";`,
120
+ `import adapter from "${specifier(SERVER_FILE, ADAPTER_FILE.replace(/\.ts$/, ""), JS)}";`,
121
+ `import { aventaraConfig } from "${specifier(SERVER_FILE, CONFIG_FILE.replace(/\.ts$/, ""), JS)}";`,
122
+ `import { aventaraContracts } from "${specifier(SERVER_FILE, `${generated}contracts.generated`, JS)}";`,
123
+ "",
124
+ "/**",
125
+ ' * Aventara on this app\'s server. Import it as "@/aventara/server", from server',
126
+ " * code only:",
127
+ " * - handlers: the route handlers src/app/api/[...aventara]/route.ts exports;",
128
+ " * - avClient: the client layer, for Server Components and Server Actions;",
129
+ " * the policy checks it as the caller `state` describes;",
130
+ " * - avApp: the application layer, for trusted server code;",
131
+ " * - ready(): builds the framework; src/instrumentation.ts awaits it at startup,",
132
+ " * so a bad configuration stops `next start`.",
133
+ " */",
134
+ "export const { handlers, avClient, avApp, ready } = Aventara({",
135
+ " ...aventaraConfig,",
136
+ " // Built on first use, never when a module is imported.",
137
+ " adapter: () => createPrismaAdapter({ ...adapter, client: prisma() }),",
138
+ " // In production, the Contracts aventara:prepare compiled (next build runs it",
139
+ " // first); in development they are compiled at startup.",
140
+ " prebuilt:",
141
+ ' process.env.NODE_ENV === "production" ? aventaraContracts : undefined,',
142
+ " // Each request's ctx.$state, read from its headers, such as the signed-in",
143
+ " // user the policy checks:",
144
+ " // state: async ({ headers }) => ({ user: await userFrom(headers) }),",
145
+ "});",
146
+ "",
147
+ ].join("\n");
148
+ }
149
+ function scaffold(input) {
150
+ const floor = lowestVersionOf(input.entry.range);
151
+ const provider = PROVIDERS[input.provider];
152
+ const driver = input.entry.drivers[input.provider];
153
+ const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
154
+ const artifact = `${NEXT.layout.generated}/discovery.artifact.ts`;
155
+ return {
156
+ files: [
157
+ {
158
+ path: SCHEMA,
159
+ content: schema({ ...input, moduleKind: "esm" }),
160
+ },
161
+ { path: "prisma.config.ts", content: prismaConfig() },
162
+ { path: CLIENT_MODULE, content: prismaClientModule(input, artifact) },
163
+ { path: CONFIG_FILE, content: aventaraConfig(artifact) },
164
+ { path: ADAPTER_FILE, content: aventaraAdapter(input, artifact) },
165
+ { path: SERVER_FILE, content: serverEntry(input, artifact) },
166
+ ],
167
+ env: { DATABASE_URL: provider.databaseUrl },
168
+ gitignore: ["/src/generated/", ...provider.gitignore],
169
+ dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
170
+ devDependencies: { prisma: floor },
171
+ prepare: prepare({
172
+ ...input,
173
+ schema: SCHEMA,
174
+ clientOutput: CLIENT_OUTPUT,
175
+ artifact,
176
+ configFile: CONFIG_FILE,
177
+ }),
178
+ nativeBuilds: provider.nativeBuilds,
179
+ nextSteps: [
180
+ ...(input.provider === "postgresql"
181
+ ? ["Set DATABASE_URL in .env to your PostgreSQL database."]
182
+ : []),
183
+ `Create the database tables: ${run} prisma db push`,
184
+ ],
185
+ };
186
+ }
187
+ export const PRISMA7_NEXT_TEMPLATES = {
188
+ scaffold,
189
+ };
@@ -0,0 +1,180 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ export const CLIENT_OUTPUT = "src/generated/prisma";
4
+ export const SCHEMA = "prisma/schema.prisma";
5
+ export function resourcesTreeBeside(artifact) {
6
+ return `${path.posix.dirname(artifact)}/resources.generated`;
7
+ }
8
+ const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
9
+ const DRIVER_PREFIX = "@prisma/adapter-";
10
+ export const PROVIDERS = {
11
+ sqlite: {
12
+ driverClass: "PrismaBetterSqlite3",
13
+ driverOptions: "{ url }",
14
+ databaseUrl: "file:./dev.db",
15
+ gitignore: ["/dev.db*"],
16
+ nativeBuilds: ["@prisma/engines", "better-sqlite3", "prisma"],
17
+ },
18
+ postgresql: {
19
+ driverClass: "PrismaPg",
20
+ driverOptions: "{ connectionString: url }",
21
+ databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
22
+ gitignore: [],
23
+ nativeBuilds: ["@prisma/engines", "prisma"],
24
+ },
25
+ };
26
+ export function specifier(from, to, js) {
27
+ const relative = path.posix.relative(path.posix.dirname(from), to);
28
+ return `${relative.startsWith(".") ? relative : `./${relative}`}${js}`;
29
+ }
30
+ export function schema(input) {
31
+ return [
32
+ "generator client {",
33
+ ` provider = "${input.entry.generators[0]}"`,
34
+ ` output = "../${CLIENT_OUTPUT}"`,
35
+ ...(input.moduleKind === "cjs" ? [' moduleFormat = "cjs"'] : []),
36
+ "}",
37
+ "",
38
+ "datasource db {",
39
+ ` provider = "${input.provider}"`,
40
+ "}",
41
+ "",
42
+ "model User {",
43
+ " id Int @id @default(autoincrement())",
44
+ " email String @unique",
45
+ " name String?",
46
+ "}",
47
+ "",
48
+ ].join("\n");
49
+ }
50
+ export function prismaConfig() {
51
+ return [
52
+ "import { existsSync } from 'node:fs';",
53
+ "import { defineConfig } from 'prisma/config';",
54
+ "",
55
+ "// Prisma reads DATABASE_URL from the environment and does not load .env itself.",
56
+ "if (existsSync('.env')) {",
57
+ " process.loadEnvFile('.env');",
58
+ "}",
59
+ "",
60
+ "export default defineConfig({",
61
+ ` schema: '${SCHEMA}',`,
62
+ " migrations: {",
63
+ " path: 'prisma/migrations',",
64
+ " },",
65
+ " datasource: {",
66
+ " url: process.env.DATABASE_URL,",
67
+ " },",
68
+ "});",
69
+ "",
70
+ ].join("\n");
71
+ }
72
+ export function discoveryImport(adapterFile, artifact, js) {
73
+ return specifier(adapterFile, artifact.replace(/\.ts$/, ""), js);
74
+ }
75
+ export function prepare(input) {
76
+ return [
77
+ input.entry.bin,
78
+ `--schema ${input.schema}`,
79
+ `--client ${input.clientOutput}`,
80
+ `--provider ${input.provider}`,
81
+ `--driver ${input.entry.drivers[input.provider]}`,
82
+ `--out ${input.artifact}`,
83
+ `--config ${input.configFile}`,
84
+ ].join(" ");
85
+ }
86
+ function schemaLocation(directory, manifest) {
87
+ for (const config of CONFIG_FILES) {
88
+ const file = path.join(directory, config);
89
+ if (existsSync(file)) {
90
+ const declared = /\bschema\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(file, "utf8"))?.[1];
91
+ if (declared !== undefined) {
92
+ return path.posix.normalize(declared);
93
+ }
94
+ }
95
+ }
96
+ const prisma = manifest.prisma;
97
+ return typeof prisma?.schema === "string"
98
+ ? path.posix.normalize(prisma.schema)
99
+ : SCHEMA;
100
+ }
101
+ function schemaFiles(directory, schemaPath) {
102
+ const full = path.join(directory, schemaPath);
103
+ if (!existsSync(full)) {
104
+ return [];
105
+ }
106
+ if (!statSync(full).isDirectory()) {
107
+ return [schemaPath];
108
+ }
109
+ return readdirSync(full, { recursive: true, withFileTypes: true })
110
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".prisma"))
111
+ .map((entry) => path
112
+ .relative(directory, path.join(entry.parentPath, entry.name))
113
+ .split(path.sep)
114
+ .join("/"))
115
+ .sort();
116
+ }
117
+ function blocks(text, kind) {
118
+ return [
119
+ ...text.matchAll(new RegExp(`^\\s*${kind}\\s+\\w+\\s*\\{([^}]*)\\}`, "gm")),
120
+ ].map((match) => match[1]);
121
+ }
122
+ function setting(block, name) {
123
+ return new RegExp(`^\\s*${name}\\s*=\\s*"([^"]*)"`, "m").exec(block)?.[1];
124
+ }
125
+ export function readProject(directory, manifest) {
126
+ const schemaPath = schemaLocation(directory, manifest);
127
+ let provider;
128
+ let generator;
129
+ let clientOutput;
130
+ for (const file of schemaFiles(directory, schemaPath)) {
131
+ const text = readFileSync(path.join(directory, file), "utf8").replace(/\/\/.*$/gm, "");
132
+ provider ??= blocks(text, "datasource")
133
+ .map((block) => setting(block, "provider"))
134
+ .find((value) => value !== undefined);
135
+ for (const block of blocks(text, "generator")) {
136
+ const declared = setting(block, "provider");
137
+ const output = setting(block, "output");
138
+ if (generator === undefined || /^prisma-client/.test(declared ?? "")) {
139
+ generator = declared;
140
+ clientOutput =
141
+ output === undefined
142
+ ? undefined
143
+ : path.posix.normalize(path.posix.join(path.posix.dirname(file), output));
144
+ }
145
+ }
146
+ }
147
+ const dependencies = [
148
+ ...Object.keys(manifest.dependencies ?? {}),
149
+ ...Object.keys(manifest.devDependencies ?? {}),
150
+ ];
151
+ return {
152
+ schema: schemaPath,
153
+ provider,
154
+ generator,
155
+ clientOutput,
156
+ drivers: dependencies.filter((name) => name.startsWith(DRIVER_PREFIX)),
157
+ };
158
+ }
159
+ export function clientShape(facts) {
160
+ return {
161
+ clientClass: "PrismaClient",
162
+ importsClient: (imported, file) => {
163
+ if (!imported.startsWith(".")) {
164
+ return imported === "@prisma/client";
165
+ }
166
+ if (facts.clientOutput === undefined) {
167
+ return false;
168
+ }
169
+ const resolved = path.posix
170
+ .join(path.posix.dirname(file), imported)
171
+ .replace(/\.(js|ts)$/, "");
172
+ return (resolved === facts.clientOutput ||
173
+ resolved === `${facts.clientOutput}/client` ||
174
+ resolved === `${facts.clientOutput}/index`);
175
+ },
176
+ skip: facts.clientOutput === undefined
177
+ ? ["src/generated"]
178
+ : ["src/generated", facts.clientOutput],
179
+ };
180
+ }