@aventara/cli 1.0.0-rc.4 → 1.0.0-rc.9
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/CHANGELOG.md +53 -0
- package/README.md +41 -3
- package/dist/catalog/host-entry.interface.js +10 -0
- package/dist/catalog/host.catalog.generated.js +32 -0
- package/dist/command/command.parser.js +55 -3
- package/dist/plan/host-plan.interface.js +1 -0
- package/dist/plan/nest-host.planner.js +131 -0
- package/dist/plan/next-host.planner.js +133 -0
- package/dist/plan/project.planner.js +81 -166
- package/dist/project/project.inspector.js +52 -13
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +46 -29
- package/dist/run/new.orchestrator.js +100 -25
- package/dist/skill/agent-skill.generated.js +2 -2
- package/dist/templates/next-server.entry.js +5 -0
- package/dist/templates/prisma7/prisma7-nest.templates.js +242 -0
- package/dist/templates/prisma7/prisma7-next.templates.js +182 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -411
- package/package.json +2 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { hostOf } from "../../catalog/host-entry.interface.js";
|
|
3
|
+
import { lowestVersionOf } from "../../catalog/range.reader.js";
|
|
4
|
+
import { EXPOSURE_DOCS } from "../docs.links.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 NEST = hostOf("nest");
|
|
8
|
+
const CONFIG_FILE = `${NEST.layout.config}.ts`;
|
|
9
|
+
const ADAPTER_FILE = `${NEST.layout.adapter}.ts`;
|
|
10
|
+
const NEW_SERVICE = {
|
|
11
|
+
file: "src/prisma.service.ts",
|
|
12
|
+
exportName: "PrismaService",
|
|
13
|
+
};
|
|
14
|
+
const NEW_MODULE = {
|
|
15
|
+
file: "src/prisma.service.ts",
|
|
16
|
+
exportName: "PrismaModule",
|
|
17
|
+
};
|
|
18
|
+
function prismaService(input) {
|
|
19
|
+
const provider = PROVIDERS[input.provider];
|
|
20
|
+
const driver = input.entry.drivers[input.provider];
|
|
21
|
+
return [
|
|
22
|
+
"import { Injectable, Module, type OnModuleDestroy } from '@nestjs/common';",
|
|
23
|
+
`import { ${provider.driverClass} } from '${driver}';`,
|
|
24
|
+
`import { PrismaClient } from '${specifier(NEW_SERVICE.file, `${input.clientOutput}/client`, input.js)}';`,
|
|
25
|
+
"",
|
|
26
|
+
"@Injectable()",
|
|
27
|
+
"export class PrismaService extends PrismaClient implements OnModuleDestroy {",
|
|
28
|
+
" constructor() {",
|
|
29
|
+
" const url = process.env.DATABASE_URL;",
|
|
30
|
+
" if (!url) {",
|
|
31
|
+
" // Thrown while Nest builds the application, before it listens.",
|
|
32
|
+
" throw new Error(",
|
|
33
|
+
" 'DATABASE_URL is not set: put it in .env (the start scripts load it) or in the environment.',",
|
|
34
|
+
" );",
|
|
35
|
+
" }",
|
|
36
|
+
` super({ adapter: new ${provider.driverClass}(${provider.driverOptions}) });`,
|
|
37
|
+
" }",
|
|
38
|
+
"",
|
|
39
|
+
" async onModuleDestroy(): Promise<void> {",
|
|
40
|
+
" await this.$disconnect();",
|
|
41
|
+
" }",
|
|
42
|
+
"}",
|
|
43
|
+
"",
|
|
44
|
+
"@Module({ providers: [PrismaService], exports: [PrismaService] })",
|
|
45
|
+
"export class PrismaModule {}",
|
|
46
|
+
"",
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
function aventaraConfig(js, artifact) {
|
|
50
|
+
return [
|
|
51
|
+
"import type { AvDiagnostic } from '@aventara/core';",
|
|
52
|
+
"import { Logger } from '@nestjs/common';",
|
|
53
|
+
`import { aventaraResources } from '${specifier(CONFIG_FILE, resourcesTreeBeside(artifact), js)}';`,
|
|
54
|
+
"",
|
|
55
|
+
"const logger = new Logger('Aventara');",
|
|
56
|
+
"",
|
|
57
|
+
"/**",
|
|
58
|
+
" * Aventara's configuration: where the API is mounted (`api`), and what you add",
|
|
59
|
+
" * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
|
|
60
|
+
" * `resources` tree).",
|
|
61
|
+
" * AventaraModule (app.module.ts) builds the framework from it and the adapter",
|
|
62
|
+
" * (src/aventara.adapter.ts). aventara-build, in the aventara:prepare script,",
|
|
63
|
+
" * runs this file to compile its Contracts, so keep its top level free of side",
|
|
64
|
+
" * effects: no connection, no request.",
|
|
65
|
+
" */",
|
|
66
|
+
"export const aventaraConfig = {",
|
|
67
|
+
` api: '${SCAFFOLD_API}',`,
|
|
68
|
+
" // Everything the adapter offers is exposed. List what to hide under",
|
|
69
|
+
" // `exemptions`, or set `exposed: false` and list what to expose instead.",
|
|
70
|
+
` // See: ${EXPOSURE_DOCS}`,
|
|
71
|
+
" exposed: true,",
|
|
72
|
+
" // Every Resource you define with defineResource() in its own file under",
|
|
73
|
+
" // src/resources/ (named <name>.resource.ts), gathered by aventara-build in",
|
|
74
|
+
" // the aventara:prepare script: a new Resource file needs no line here. When",
|
|
75
|
+
" // a Resource gives a layer a block, that layer takes its tree once, e.g.",
|
|
76
|
+
" // client: { resources: aventaraResources.client },",
|
|
77
|
+
" resources: aventaraResources.root,",
|
|
78
|
+
" // When something fails inside the server, the caller gets a plain 500 and",
|
|
79
|
+
" // Aventara logs one line naming the request. This logs the error itself,",
|
|
80
|
+
" // with its stack, through Nest's logger.",
|
|
81
|
+
" diagnostics: (diagnostic: AvDiagnostic) => {",
|
|
82
|
+
" const { error } = diagnostic;",
|
|
83
|
+
" logger.error(",
|
|
84
|
+
` \`\${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
|
|
85
|
+
" error instanceof Error ? error.stack : String(error),",
|
|
86
|
+
" );",
|
|
87
|
+
" },",
|
|
88
|
+
"} as const;",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
function aventaraAdapter(input) {
|
|
93
|
+
return [
|
|
94
|
+
`import { describePrismaAdapter } from '${input.entry.adapterPackage}';`,
|
|
95
|
+
`import { discovery } from '${discoveryImport(ADAPTER_FILE, input.artifact, input.js)}';`,
|
|
96
|
+
"",
|
|
97
|
+
"/**",
|
|
98
|
+
" * The Prisma adapter's description: your schema's model, as the aventara:prepare",
|
|
99
|
+
" * script discovered it (`discovery`), and the database it runs on. aventara-build",
|
|
100
|
+
" * compiles the Contracts from it; app.module.ts builds the adapter from it with",
|
|
101
|
+
" * the Prisma client.",
|
|
102
|
+
" */",
|
|
103
|
+
"export default describePrismaAdapter({",
|
|
104
|
+
" discovery,",
|
|
105
|
+
` provider: '${input.provider}',`,
|
|
106
|
+
` driver: '${input.entry.drivers[input.provider]}',`,
|
|
107
|
+
"});",
|
|
108
|
+
"",
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
function appModule(service, module, adapterPackage, js) {
|
|
112
|
+
const from = "src/app.module.ts";
|
|
113
|
+
const sameFile = service.file === module.file;
|
|
114
|
+
return {
|
|
115
|
+
imports: [
|
|
116
|
+
`import { createPrismaAdapter } from '${adapterPackage}';`,
|
|
117
|
+
`import adapter from '${specifier(from, ADAPTER_FILE.replace(/\.ts$/, ""), js)}';`,
|
|
118
|
+
`import { aventaraConfig } from '${specifier(from, CONFIG_FILE.replace(/\.ts$/, ""), js)}';`,
|
|
119
|
+
...(sameFile
|
|
120
|
+
? [
|
|
121
|
+
`import { ${module.exportName}, ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
|
|
122
|
+
]
|
|
123
|
+
: [
|
|
124
|
+
`import { ${module.exportName} } from '${specifier(from, module.file.replace(/\.ts$/, ""), js)}';`,
|
|
125
|
+
`import { ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
|
|
126
|
+
]),
|
|
127
|
+
],
|
|
128
|
+
entries: [
|
|
129
|
+
`${module.exportName},`,
|
|
130
|
+
[
|
|
131
|
+
"AventaraModule.forRootAsync({",
|
|
132
|
+
` imports: [${module.exportName}],`,
|
|
133
|
+
` inject: [${service.exportName}],`,
|
|
134
|
+
` useFactory: async (prisma: ${service.exportName}) => ({`,
|
|
135
|
+
" config: {",
|
|
136
|
+
" ...aventaraConfig,",
|
|
137
|
+
" adapter: await createPrismaAdapter({",
|
|
138
|
+
" ...adapter,",
|
|
139
|
+
service.clientMember === undefined
|
|
140
|
+
? " client: prisma,"
|
|
141
|
+
: ` client: prisma.${service.clientMember},`,
|
|
142
|
+
" }),",
|
|
143
|
+
" },",
|
|
144
|
+
" }),",
|
|
145
|
+
"}),",
|
|
146
|
+
].join("\n"),
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function scaffold(input) {
|
|
151
|
+
const js = input.moduleKind === "esm" ? ".js" : "";
|
|
152
|
+
const floor = lowestVersionOf(input.entry.range);
|
|
153
|
+
const provider = PROVIDERS[input.provider];
|
|
154
|
+
const driver = input.entry.drivers[input.provider];
|
|
155
|
+
const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
|
|
156
|
+
const artifact = `${NEST.layout.generated}/discovery.artifact.ts`;
|
|
157
|
+
return {
|
|
158
|
+
files: [
|
|
159
|
+
{ path: SCHEMA, content: schema(input) },
|
|
160
|
+
{ path: "prisma.config.ts", content: prismaConfig() },
|
|
161
|
+
{
|
|
162
|
+
path: NEW_SERVICE.file,
|
|
163
|
+
content: prismaService({ ...input, clientOutput: CLIENT_OUTPUT, js }),
|
|
164
|
+
},
|
|
165
|
+
{ path: CONFIG_FILE, content: aventaraConfig(js, artifact) },
|
|
166
|
+
{
|
|
167
|
+
path: ADAPTER_FILE,
|
|
168
|
+
content: aventaraAdapter({ ...input, artifact, js }),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
env: { DATABASE_URL: provider.databaseUrl },
|
|
172
|
+
gitignore: ["/src/generated/", ...provider.gitignore],
|
|
173
|
+
dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
|
|
174
|
+
devDependencies: { prisma: floor },
|
|
175
|
+
prepare: prepare({
|
|
176
|
+
...input,
|
|
177
|
+
schema: SCHEMA,
|
|
178
|
+
clientOutput: CLIENT_OUTPUT,
|
|
179
|
+
artifact,
|
|
180
|
+
configFile: CONFIG_FILE,
|
|
181
|
+
}),
|
|
182
|
+
appModule: appModule(NEW_SERVICE, NEW_MODULE, input.entry.adapterPackage, js),
|
|
183
|
+
nativeBuilds: provider.nativeBuilds,
|
|
184
|
+
nextSteps: [
|
|
185
|
+
...(input.provider === "postgresql"
|
|
186
|
+
? ["Set DATABASE_URL in .env to your PostgreSQL database."]
|
|
187
|
+
: []),
|
|
188
|
+
`Create the database tables: ${run} prisma db push`,
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function reuse(input) {
|
|
193
|
+
const js = input.moduleKind === "esm" ? ".js" : "";
|
|
194
|
+
const { facts } = input;
|
|
195
|
+
return {
|
|
196
|
+
files: [
|
|
197
|
+
...(input.writeService
|
|
198
|
+
? [
|
|
199
|
+
{
|
|
200
|
+
path: NEW_SERVICE.file,
|
|
201
|
+
content: prismaService({
|
|
202
|
+
entry: input.entry,
|
|
203
|
+
provider: facts.provider,
|
|
204
|
+
clientOutput: facts.clientOutput,
|
|
205
|
+
js,
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
: []),
|
|
210
|
+
{ path: CONFIG_FILE, content: aventaraConfig(js, input.artifact) },
|
|
211
|
+
{
|
|
212
|
+
path: ADAPTER_FILE,
|
|
213
|
+
content: aventaraAdapter({
|
|
214
|
+
entry: input.entry,
|
|
215
|
+
provider: facts.provider,
|
|
216
|
+
artifact: input.artifact,
|
|
217
|
+
js,
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
env: {},
|
|
222
|
+
gitignore: [`/${path.posix.dirname(input.artifact)}/`],
|
|
223
|
+
dependencies: {},
|
|
224
|
+
devDependencies: {},
|
|
225
|
+
prepare: prepare({
|
|
226
|
+
entry: input.entry,
|
|
227
|
+
schema: facts.schema,
|
|
228
|
+
clientOutput: facts.clientOutput,
|
|
229
|
+
provider: facts.provider,
|
|
230
|
+
artifact: input.artifact,
|
|
231
|
+
configFile: CONFIG_FILE,
|
|
232
|
+
}),
|
|
233
|
+
appModule: appModule(input.service, input.module, input.entry.adapterPackage, js),
|
|
234
|
+
nativeBuilds: [],
|
|
235
|
+
nextSteps: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export const PRISMA7_NEST_TEMPLATES = {
|
|
239
|
+
scaffold,
|
|
240
|
+
newService: { service: NEW_SERVICE, module: NEW_MODULE },
|
|
241
|
+
reuse,
|
|
242
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
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) {
|
|
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
|
+
"",
|
|
20
|
+
"const globalForPrisma = globalThis as typeof globalThis & {",
|
|
21
|
+
" prisma?: PrismaClient;",
|
|
22
|
+
"};",
|
|
23
|
+
"",
|
|
24
|
+
"/**",
|
|
25
|
+
" * The app's one PrismaClient, made on first use: importing this module connects",
|
|
26
|
+
" * to nothing. It is kept on globalThis, so a reload in `next dev` reuses it",
|
|
27
|
+
" * instead of opening another connection.",
|
|
28
|
+
" */",
|
|
29
|
+
"export function prisma(): PrismaClient {",
|
|
30
|
+
" if (globalForPrisma.prisma === undefined) {",
|
|
31
|
+
" const url = process.env.DATABASE_URL;",
|
|
32
|
+
" if (!url) {",
|
|
33
|
+
" throw new Error(",
|
|
34
|
+
' "DATABASE_URL is not set: put it in .env (Next.js loads it) or in the environment.",',
|
|
35
|
+
" );",
|
|
36
|
+
" }",
|
|
37
|
+
" globalForPrisma.prisma = new PrismaClient({",
|
|
38
|
+
` adapter: new ${provider.driverClass}(${provider.driverOptions}),`,
|
|
39
|
+
" });",
|
|
40
|
+
" }",
|
|
41
|
+
" return globalForPrisma.prisma;",
|
|
42
|
+
"}",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function aventaraConfig(artifact) {
|
|
47
|
+
return [
|
|
48
|
+
'import type { AvDiagnostic } from "@aventara/core";',
|
|
49
|
+
`import { aventaraResources } from "${specifier(CONFIG_FILE, resourcesTreeBeside(artifact), JS)}";`,
|
|
50
|
+
"",
|
|
51
|
+
"/**",
|
|
52
|
+
" * Aventara's configuration: where the API is mounted (`api`), and what you add",
|
|
53
|
+
" * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
|
|
54
|
+
" * `resources` tree).",
|
|
55
|
+
" * server.ts builds the framework from it and the adapter (adapter.ts).",
|
|
56
|
+
" * aventara-build, in the aventara:prepare script, runs this file to compile its",
|
|
57
|
+
" * Contracts and write the browser client, so keep its top level free of side",
|
|
58
|
+
" * effects: no connection, no request.",
|
|
59
|
+
" */",
|
|
60
|
+
"export const aventaraConfig = {",
|
|
61
|
+
" // The route files under src/app/api/[...aventara]/ serve it: keep the two in step.",
|
|
62
|
+
` api: "${SCAFFOLD_API}",`,
|
|
63
|
+
" // Everything the adapter offers is exposed. List what to hide under",
|
|
64
|
+
" // `exemptions`, or set `exposed: false` and list what to expose instead.",
|
|
65
|
+
` // See: ${EXPOSURE_DOCS}`,
|
|
66
|
+
" exposed: true,",
|
|
67
|
+
" // Every Resource you define with defineResource() in its own file under",
|
|
68
|
+
" // src/resources/ (named <name>.resource.ts), gathered by aventara-build in",
|
|
69
|
+
" // the aventara:prepare script: a new Resource file needs no line here. When",
|
|
70
|
+
" // a Resource gives a layer a block, that layer takes its tree once, e.g.",
|
|
71
|
+
" // client: { resources: aventaraResources.client },",
|
|
72
|
+
" resources: aventaraResources.root,",
|
|
73
|
+
" // When something fails inside the server, the caller gets a plain 500 and",
|
|
74
|
+
" // Aventara logs one line naming the request. This logs the error itself,",
|
|
75
|
+
" // with its stack, on the server's console.",
|
|
76
|
+
" diagnostics: (diagnostic: AvDiagnostic) => {",
|
|
77
|
+
" const { error } = diagnostic;",
|
|
78
|
+
" console.error(",
|
|
79
|
+
` \`Aventara \${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
|
|
80
|
+
" error instanceof Error ? error.stack : String(error),",
|
|
81
|
+
" );",
|
|
82
|
+
" },",
|
|
83
|
+
"} as const;",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
function aventaraAdapter(input, artifact) {
|
|
88
|
+
return [
|
|
89
|
+
`import { describePrismaAdapter } from "${input.entry.adapterPackage}";`,
|
|
90
|
+
`import { discovery } from "${discoveryImport(ADAPTER_FILE, artifact, JS)}";`,
|
|
91
|
+
"",
|
|
92
|
+
"/**",
|
|
93
|
+
" * The Prisma adapter's description: your schema's model, as the aventara:prepare",
|
|
94
|
+
" * script discovered it (`discovery`), and the database it runs on. aventara-build",
|
|
95
|
+
" * compiles the Contracts from it; server.ts builds the adapter from it with the",
|
|
96
|
+
" * Prisma client.",
|
|
97
|
+
" */",
|
|
98
|
+
"export default describePrismaAdapter({",
|
|
99
|
+
" discovery,",
|
|
100
|
+
` provider: "${input.provider}",`,
|
|
101
|
+
` driver: "${input.entry.drivers[input.provider]}",`,
|
|
102
|
+
"});",
|
|
103
|
+
"",
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
function serverEntry(input, artifact) {
|
|
107
|
+
const generated = artifact.replace(/[^/]+$/, "");
|
|
108
|
+
return [
|
|
109
|
+
'import "server-only";',
|
|
110
|
+
'import { Aventara } from "@aventara/next";',
|
|
111
|
+
`import { createPrismaAdapter } from "${input.entry.adapterPackage}";`,
|
|
112
|
+
`import { prisma } from "${specifier(SERVER_FILE, CLIENT_MODULE.replace(/\.ts$/, ""), JS)}";`,
|
|
113
|
+
`import adapter from "${specifier(SERVER_FILE, ADAPTER_FILE.replace(/\.ts$/, ""), JS)}";`,
|
|
114
|
+
`import { aventaraConfig } from "${specifier(SERVER_FILE, CONFIG_FILE.replace(/\.ts$/, ""), JS)}";`,
|
|
115
|
+
`import { aventaraContracts } from "${specifier(SERVER_FILE, `${generated}contracts.generated`, JS)}";`,
|
|
116
|
+
"",
|
|
117
|
+
"/**",
|
|
118
|
+
' * Aventara on this app\'s server. Import it as "@/aventara/server", from server',
|
|
119
|
+
" * code only:",
|
|
120
|
+
" * - handlers: the route handlers src/app/api/[...aventara]/route.ts exports;",
|
|
121
|
+
" * - avClient: the client layer, for Server Components and Server Actions;",
|
|
122
|
+
" * the policy checks it as the caller `state` describes;",
|
|
123
|
+
" * - avApp: the application layer, for trusted server code;",
|
|
124
|
+
" * - ready(): builds the framework; src/instrumentation.ts awaits it at startup,",
|
|
125
|
+
" * so a bad configuration stops `next start`.",
|
|
126
|
+
" */",
|
|
127
|
+
"export const { handlers, avClient, avApp, ready } = Aventara({",
|
|
128
|
+
" ...aventaraConfig,",
|
|
129
|
+
" // Built on first use, never when a module is imported.",
|
|
130
|
+
" adapter: () => createPrismaAdapter({ ...adapter, client: prisma() }),",
|
|
131
|
+
" // In production, the Contracts aventara:prepare compiled (next build runs it",
|
|
132
|
+
" // first); in development they are compiled at startup.",
|
|
133
|
+
" prebuilt:",
|
|
134
|
+
' process.env.NODE_ENV === "production" ? aventaraContracts : undefined,',
|
|
135
|
+
" // Each request's ctx.$state, read from its headers, such as the signed-in",
|
|
136
|
+
" // user the policy checks:",
|
|
137
|
+
" // state: async ({ headers }) => ({ user: await userFrom(headers) }),",
|
|
138
|
+
"});",
|
|
139
|
+
"",
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
function scaffold(input) {
|
|
143
|
+
const floor = lowestVersionOf(input.entry.range);
|
|
144
|
+
const provider = PROVIDERS[input.provider];
|
|
145
|
+
const driver = input.entry.drivers[input.provider];
|
|
146
|
+
const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
|
|
147
|
+
const artifact = `${NEXT.layout.generated}/discovery.artifact.ts`;
|
|
148
|
+
return {
|
|
149
|
+
files: [
|
|
150
|
+
{
|
|
151
|
+
path: SCHEMA,
|
|
152
|
+
content: schema({ ...input, moduleKind: "esm" }),
|
|
153
|
+
},
|
|
154
|
+
{ path: "prisma.config.ts", content: prismaConfig() },
|
|
155
|
+
{ path: CLIENT_MODULE, content: prismaClientModule(input) },
|
|
156
|
+
{ path: CONFIG_FILE, content: aventaraConfig(artifact) },
|
|
157
|
+
{ path: ADAPTER_FILE, content: aventaraAdapter(input, artifact) },
|
|
158
|
+
{ path: SERVER_FILE, content: serverEntry(input, artifact) },
|
|
159
|
+
],
|
|
160
|
+
env: { DATABASE_URL: provider.databaseUrl },
|
|
161
|
+
gitignore: ["/src/generated/", ...provider.gitignore],
|
|
162
|
+
dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
|
|
163
|
+
devDependencies: { prisma: floor },
|
|
164
|
+
prepare: prepare({
|
|
165
|
+
...input,
|
|
166
|
+
schema: SCHEMA,
|
|
167
|
+
clientOutput: CLIENT_OUTPUT,
|
|
168
|
+
artifact,
|
|
169
|
+
configFile: CONFIG_FILE,
|
|
170
|
+
}),
|
|
171
|
+
nativeBuilds: provider.nativeBuilds,
|
|
172
|
+
nextSteps: [
|
|
173
|
+
...(input.provider === "postgresql"
|
|
174
|
+
? ["Set DATABASE_URL in .env to your PostgreSQL database."]
|
|
175
|
+
: []),
|
|
176
|
+
`Create the database tables: ${run} prisma db push`,
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export const PRISMA7_NEXT_TEMPLATES = {
|
|
181
|
+
scaffold,
|
|
182
|
+
};
|
|
@@ -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
|
+
}
|