@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.1
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 +231 -0
- package/README.md +50 -42
- package/dist/apply/conflict.confirmer.js +4 -4
- package/dist/cli.js +14 -14
- package/dist/command/command.parser.js +10 -10
- package/dist/plan/project.planner.js +2 -4
- package/dist/project/project.inspector.js +8 -8
- package/dist/run/init.orchestrator.js +8 -8
- package/dist/run/new.orchestrator.js +5 -5
- package/dist/templates/docs.links.js +2 -1
- package/dist/templates/prisma7/prisma7.templates.js +26 -5
- package/dist/templates/template.registry.js +1 -1
- package/dist/wizard/answer.resolver.js +5 -5
- package/dist/wizard/readline.prompter.js +3 -3
- package/dist/wizard/wizard.questions.js +9 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,236 @@
|
|
|
1
1
|
# @aventara/cli
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e3d06f: Breaking: the configuration has a new shape. Every layer (the root, `application` and `client`) now holds the
|
|
8
|
+
same six keys: `exposed`, `transactionsStyle`, `limits`, `pipelines`, `resources` and `exemptions`. Everything about
|
|
9
|
+
one Resource lives under `resources.<Resource>`: its fields, and each field's `definition` and `pipelines`. The old
|
|
10
|
+
keys are refused when the framework starts, with a message that names where each one moved:
|
|
11
|
+
|
|
12
|
+
| Before (rc.0) | Now (rc.1) |
|
|
13
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
14
|
+
| `fields: { Post: { url: { type, nullable, … } } }` | `resources: { Post: { fields: { url: { definition: { type, nullable, … } } } } }` |
|
|
15
|
+
| `behaviors: { Post: { fields: { slug: { create } } } }` | `resources: { Post: { fields: { slug: { pipelines: { compute: { create } } } } } }` |
|
|
16
|
+
| `transactions: "none"` | `transactionsStyle: "none"` |
|
|
17
|
+
| `restrictions` | `exposed` + `exemptions` (see below) |
|
|
18
|
+
| `@aventara/core/json` | `@aventara/core/avz` |
|
|
19
|
+
|
|
20
|
+
Before:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
createFramework({
|
|
24
|
+
api: "/api",
|
|
25
|
+
adapter,
|
|
26
|
+
transactions: "interactive",
|
|
27
|
+
fields: {
|
|
28
|
+
Post: {
|
|
29
|
+
url: {
|
|
30
|
+
type: { scalar: "string" },
|
|
31
|
+
nullable: false,
|
|
32
|
+
list: false,
|
|
33
|
+
lifecycle: ["VIRTUAL"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
behaviors: {
|
|
38
|
+
Post: {
|
|
39
|
+
fields: {
|
|
40
|
+
url: {
|
|
41
|
+
read: {
|
|
42
|
+
dependsOn: ["slug"],
|
|
43
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
createFramework({
|
|
56
|
+
api: "/api",
|
|
57
|
+
adapter,
|
|
58
|
+
transactionsStyle: "interactive",
|
|
59
|
+
resources: {
|
|
60
|
+
Post: {
|
|
61
|
+
fields: {
|
|
62
|
+
url: {
|
|
63
|
+
definition: {
|
|
64
|
+
type: { scalar: "string" },
|
|
65
|
+
nullable: false,
|
|
66
|
+
list: false,
|
|
67
|
+
lifecycle: ["VIRTUAL"],
|
|
68
|
+
format: "url",
|
|
69
|
+
},
|
|
70
|
+
pipelines: {
|
|
71
|
+
compute: {
|
|
72
|
+
read: {
|
|
73
|
+
dependsOn: ["slug"],
|
|
74
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
What else changed:
|
|
86
|
+
|
|
87
|
+
- A field is defined at the root only. A `definition` in `application` or `client` is refused
|
|
88
|
+
(`CONFIG_FORBIDDEN_IN_OVERLAY`). Define the field
|
|
89
|
+
at the root; `application` and `client` can still add compute stages to it.
|
|
90
|
+
- Computed values chain. When the root and an overlay both compute a field, the root's stage runs first and the
|
|
91
|
+
overlay's receives its result as `value`. A stage can no longer be turned off with `false`. Returning `undefined`
|
|
92
|
+
keeps the current value. An upsert computes its create branch with `compute.create` and its update branch with
|
|
93
|
+
`compute.update`.
|
|
94
|
+
- A discovered field keeps the adapter's facts. On a field the adapter discovered you may restate its own `type`,
|
|
95
|
+
`nullable` and `list`, and give a Json field its shape (`definition.json`). Setting a different type, any `format`
|
|
96
|
+
or any `capabilities` on it is refused. `format` is for fields you add, and it must be one of zod's named string
|
|
97
|
+
formats (`"email"`, `"url"`, `"uuid"`, …); any other name is a type error and refused at startup.
|
|
98
|
+
- A misplaced key is a type error. In a configuration written inline in `createFramework({ ... })` or
|
|
99
|
+
`AventaraModule.forRoot`, a key that has no place there (a typo, an old key, `compute` on a relation field, a field
|
|
100
|
+
name neither your schema nor the root has) is now a type error at that key, and still refused at startup.
|
|
101
|
+
- A Resource whose name starts with `$` is refused at startup (`$` names are the framework's).
|
|
102
|
+
- The Json shape builder moved from `@aventara/core/json` to `@aventara/core/avz`. Change the import; `AvZ` is
|
|
103
|
+
the same.
|
|
104
|
+
|
|
105
|
+
`exposed` and `exemptions`, which replace `restrictions`, have their own entry.
|
|
106
|
+
|
|
107
|
+
- 6317811: Breaking: `exposed` and `exemptions` replace `restrictions`. Every layer (the root, `application` and `client`)
|
|
108
|
+
says what it exposes with two keys: `exposed`, the layer's default, and `exemptions`, the entries that are the
|
|
109
|
+
opposite of it.
|
|
110
|
+
|
|
111
|
+
- `exposed: true` (the root's default) exposes everything the layer above offers, and `exemptions` lists what to
|
|
112
|
+
hide. `exposed: false` exposes nothing, and `exemptions` lists the only things to expose.
|
|
113
|
+
- In `exemptions`, `true` exempts an entry whole, `false` and `[]` exempt nothing, a list exempts the listed
|
|
114
|
+
entries one level down, and an object goes one level deeper. A Resource takes `true`, `false` or
|
|
115
|
+
`{ operations, fields }`. Operations are nested: `operations: { find: ["many"], update: { unique: true } }`.
|
|
116
|
+
A field takes a list of capability names, or an object per capability whose token lists name filter operators,
|
|
117
|
+
directives, reducers or pagination kinds.
|
|
118
|
+
- A list now always means "exempt these". rc.0's `filter: ["equals"]` meant "keep only `equals`"; under
|
|
119
|
+
`exposed: true` the same intent is the list of the operators to remove.
|
|
120
|
+
- Overlays only narrow. `application` and `client` start from the root's view. An overlay can no longer bring
|
|
121
|
+
back what the root hid (rc.0's `hidden: false`); that is now refused at startup, and the message names the other
|
|
122
|
+
overlay to hide it in instead. Expose an entry at the root and hide it in the overlay that must not see it.
|
|
123
|
+
- An overlay that sets `exemptions` must also set its own `exposed`.
|
|
124
|
+
- Relation positions are the adapter's: an exemption can no longer narrow where a relation is projected or written.
|
|
125
|
+
- A unique operation (`find.unique`, `update.unique`, `delete.unique`, `upsert.unique`) needs an identifier. Picking
|
|
126
|
+
one without the identifier's fields, or hiding every identifier while one stays exposed, is refused at startup
|
|
127
|
+
with `COMPILER_MISSING_IDENTIFIER` instead of the operation silently disappearing.
|
|
128
|
+
|
|
129
|
+
Every name in `exemptions` must exist, in both styles: a Resource, field, family, variant, capability or token the
|
|
130
|
+
adapter does not offer is refused at startup with its path (`COMPILER_UNKNOWN_RESOURCE`, `COMPILER_UNKNOWN_FIELD`,
|
|
131
|
+
`COMPILER_UNAVAILABLE_OPERATION`, `COMPILER_UNAVAILABLE_CAPABILITY`), and in a configuration written inline in
|
|
132
|
+
`createFramework({ ... })` it is a type error first. Under `exposed: true`, hiding in an overlay what the root
|
|
133
|
+
already hid does nothing.
|
|
134
|
+
|
|
135
|
+
Before (rc.0):
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
createFramework({
|
|
139
|
+
api: "/api",
|
|
140
|
+
adapter,
|
|
141
|
+
restrictions: {
|
|
142
|
+
User: {
|
|
143
|
+
fields: {
|
|
144
|
+
passwordHash: { hidden: true },
|
|
145
|
+
email: { capabilities: { filter: ["equals"] } },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
client: {
|
|
150
|
+
restrictions: {
|
|
151
|
+
AuditLog: { hidden: true },
|
|
152
|
+
Post: { operations: { delete: { unique: false } } },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
After (rc.1):
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
createFramework({
|
|
162
|
+
api: "/api",
|
|
163
|
+
adapter,
|
|
164
|
+
// The root: `exposed` defaults to true, so these are hidden from every origin.
|
|
165
|
+
exemptions: {
|
|
166
|
+
resources: {
|
|
167
|
+
User: {
|
|
168
|
+
fields: {
|
|
169
|
+
passwordHash: true,
|
|
170
|
+
email: { filter: ["contains", "startsWith"] }, // remove every operator but `equals`
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
client: {
|
|
176
|
+
exposed: true,
|
|
177
|
+
exemptions: {
|
|
178
|
+
resources: {
|
|
179
|
+
AuditLog: true,
|
|
180
|
+
Post: { operations: { delete: ["unique"] } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or expose only what a frontend calls:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
client: {
|
|
191
|
+
exposed: false,
|
|
192
|
+
exemptions: {
|
|
193
|
+
resources: {
|
|
194
|
+
Post: {
|
|
195
|
+
operations: { find: ["many", "unique"], create: ["one"] },
|
|
196
|
+
fields: { id: ["select"], title: ["select", "create"], body: ["select", "create"] },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`aventara init` and `aventara new` now write `exposed: true` into `src/aventara.config.ts`, with a comment that
|
|
204
|
+
links the exposure documentation.
|
|
205
|
+
|
|
206
|
+
- 9c52482: A `$connect` to a row that does not exist now answers `404 A2003` (not found), the same as reading a missing row.
|
|
207
|
+
Before, it answered `422 A2004`. This covers a to-one `$connect` in `create`, `update` and `upsert`, a to-many
|
|
208
|
+
`$connect`, and the same writes inside a transaction plan, where `cause.operation` names the step. The message is
|
|
209
|
+
unchanged: "The operation's arguments referenced a record that does not exist." On `update.first`, the same mistake
|
|
210
|
+
answered `A2014` before, as if the row had changed underneath; it now answers `A2003` too, and a duplicate unique value
|
|
211
|
+
written through `update.first` answers `409 A2008` instead of `A3001`.
|
|
212
|
+
|
|
213
|
+
Clearer top-level messages for an unknown Resource or operation. `A2001` now says "The requested Resource is not
|
|
214
|
+
available." and `A2002` says "The requested operation is not available on this Resource.", instead of "Operation
|
|
215
|
+
arguments failed framework validation.", which stays the message of `A2004`, `A2007` and `A2009`. A transaction step
|
|
216
|
+
whose operation is outside the standard grammar now carries the same issue message as a single call, naming the
|
|
217
|
+
operation and the Resource (for example `Operation "dance.one" is not available on Resource "posts".`), instead of
|
|
218
|
+
"Operation is not part of the standard operation grammar."
|
|
219
|
+
|
|
220
|
+
A `$ref` with no `fingerprint` is `V1000` (required value missing) and answers `422 A2004`. Before, it was `V1017`
|
|
221
|
+
(fingerprint mismatch) under `A2007`. A fingerprint that is present but wrong is still `V1017`.
|
|
222
|
+
|
|
223
|
+
`aventara init` and `aventara new` link to the documentation at https://aventara-framework.vercel.app. The old
|
|
224
|
+
address redirects, so links already written into projects keep working.
|
|
225
|
+
|
|
226
|
+
- 8db76e7: `aventara new` and `aventara init` write a `diagnostics` sink into `src/aventara.config.ts`. When something fails
|
|
227
|
+
inside the server, the caller gets a plain 500 and the Nest log said only which request failed; the raw error went to
|
|
228
|
+
a sink the scaffold did not set. Now the sink logs the raw error with its stack through Nest's `Logger` (context
|
|
229
|
+
`Aventara`), at error level, right after that line. Replace it with your own reporter when you have one.
|
|
230
|
+
|
|
231
|
+
- `aventara new` and `aventara init` write `api: '/api'` into `src/aventara.config.ts` (the root key was renamed from
|
|
232
|
+
`entrypoint`).
|
|
233
|
+
|
|
3
234
|
## 1.0.0-rc.0
|
|
4
235
|
|
|
5
236
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ aventara new my-api # a new NestJS project with Aventara
|
|
|
8
8
|
aventara init # Aventara added to the NestJS 12 project in this directory
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side
|
|
12
|
-
`framework.client.ts` and the generated client
|
|
11
|
+
It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side
|
|
12
|
+
(`framework.client.ts` and the generated client) belongs to `@aventara/client` (`npx @aventara/client@rc init`).
|
|
13
13
|
|
|
14
14
|
## `aventara new <name>`
|
|
15
15
|
|
|
@@ -17,22 +17,24 @@ Runs the pinned `nest new` (`npx -y @nestjs/cli@12.0.8 new …`, or `pnpm dlx
|
|
|
17
17
|
`src/app.module.ts` is what that version writes, then runs exactly `aventara init`'s pipeline over the new project and
|
|
18
18
|
installs it. A directory that exists and is not empty is refused before anything runs.
|
|
19
19
|
|
|
20
|
-
Once installed, the project gets its first git commit, `Initial Aventara Scaffold
|
|
21
|
-
(it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
|
|
20
|
+
Once installed, the project gets its first git commit, `Initial Aventara Scaffold`, with the lockfile in it and
|
|
21
|
+
without `.env` (it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
|
|
22
22
|
`user.name`/`user.email`, or when the directory is already inside a git repository (a monorepo: commit it there).
|
|
23
23
|
`--skip-git` makes no repository and no commit.
|
|
24
24
|
|
|
25
25
|
## `aventara init`
|
|
26
26
|
|
|
27
|
-
Runs inside an existing
|
|
28
|
-
lockfile, a project already initialized
|
|
27
|
+
Runs inside an existing NestJS 12 project. It refuses a directory with no `package.json`, another Nest major,
|
|
28
|
+
TypeScript 7, a yarn-only lockfile, or a project already initialized.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
In a project with no ORM yet, the ORM and the database are asked (or taken from the flags), and everything below is
|
|
31
|
+
written.
|
|
32
|
+
|
|
33
|
+
In a project that already uses Prisma, the installed Prisma must be one an Aventara adapter supports, or the run is
|
|
34
|
+
refused in one sentence with nothing written. Today that is Prisma 7 (`^7.10.0`) on SQLite or PostgreSQL, with a driver
|
|
35
|
+
adapter and the `prisma-client` generator. Your schema, Prisma config and service are reused and never written, and
|
|
36
|
+
your `.env` only gains a `CORS_ORIGINS` line at its end when it has none. Init finds the class extending `PrismaClient`
|
|
37
|
+
and the module exporting it, or asks.
|
|
36
38
|
|
|
37
39
|
### Questions and flags
|
|
38
40
|
|
|
@@ -42,20 +44,20 @@ possible answer is shown, not asked.
|
|
|
42
44
|
|
|
43
45
|
| Flag | Question | Default |
|
|
44
46
|
|---|---|---|
|
|
45
|
-
| `<name>` (`new` only) | The project directory. |
|
|
47
|
+
| `<name>` (`new` only) | The project directory. | required |
|
|
46
48
|
| `--orm <alias>` | The ORM, as an adapter's alias: `prisma7` (Prisma 7, `@aventara/prisma7-adapter`). `--help` lists the aliases that exist. | the only one there is |
|
|
47
49
|
| `--db <provider>` | The database: the adapter's providers (`sqlite`, `postgresql`). | `sqlite` |
|
|
48
|
-
| `--package-manager <npm\|pnpm>` |
|
|
50
|
+
| `--package-manager <npm\|pnpm>` | The package manager. | the lockfile's, else the one that launched the CLI, else npm |
|
|
49
51
|
| `--prisma-service <path#Export>` (`init`) | Your Prisma service, when more than one, or none, is found. | the one found; none → write `src/prisma.service.ts` |
|
|
50
52
|
| `--prisma-module <path#Export>` (`init`) | The Nest module that provides and exports it. | the one found |
|
|
51
53
|
| `--skip-install` | Write the files; print the install instead of running it. | |
|
|
52
54
|
| `--skip-git` (`new`) | Passed to `nest new`. | |
|
|
53
|
-
| `-y`, `--yes` | Accept every unanswered default
|
|
55
|
+
| `-y`, `--yes` | Accept every unanswered default, and replace existing content that differs. | |
|
|
54
56
|
| `--verbose` | Show the output of `nest new` and the install as they run. | |
|
|
55
57
|
|
|
56
|
-
While it works, each step is shown on stderr
|
|
57
|
-
|
|
58
|
-
`
|
|
58
|
+
While it works, each step is shown on stderr, such as `[3/4] Installing dependencies (npm install)`. On a terminal it
|
|
59
|
+
has a spinner and the seconds so far; in a pipe, in CI (`CI`) or with `NO_COLOR` it is one plain line per step
|
|
60
|
+
(`… done (12.4s)`). The output of `nest new` and the install is shown only if it fails, or with `--verbose`.
|
|
59
61
|
|
|
60
62
|
Existing content that differs from what would be written (a `.env` key, a script, a file) is listed, and replaced only
|
|
61
63
|
when you confirm, or with `--yes`; where nobody can be asked, the run stops and touches nothing.
|
|
@@ -64,17 +66,17 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
64
66
|
|
|
65
67
|
| Path | Owner afterwards |
|
|
66
68
|
|---|---|
|
|
67
|
-
| `prisma/schema.prisma
|
|
68
|
-
| `prisma.config.ts
|
|
69
|
-
| `src/prisma.service.ts
|
|
70
|
-
| `src/aventara.config.ts
|
|
71
|
-
| `src/app.module.ts
|
|
72
|
-
| `test/app.e2e-spec.ts
|
|
73
|
-
| `src/main.ts
|
|
69
|
+
| `prisma/schema.prisma`: `prisma-client` generating into `src/generated/prisma`, a `User` model | you |
|
|
70
|
+
| `prisma.config.ts`: loads `.env` itself (Prisma does not) | you |
|
|
71
|
+
| `src/prisma.service.ts`: `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
|
|
72
|
+
| `src/aventara.config.ts`: `aventaraConfig(prisma)`: the `api` mount path (`/api`), a `diagnostics` sink that logs the raw error of a server failure, with its stack, through Nest's `Logger`, and the `exposed`/`exemptions`, `pipelines` and `resources` you add | you |
|
|
73
|
+
| `src/app.module.ts`: one edit that adds the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
|
|
74
|
+
| `test/app.e2e-spec.ts`: one edit that adds a `GET /api/_contract` → 200 test after Nest's `GET /` test; left alone when the file is not Nest's | you |
|
|
75
|
+
| `src/main.ts`: one edit after `NestFactory.create`: `app.enableCors(…)` for the comma-separated origins in `CORS_ORIGINS`, and CORS off when it is unset or empty, then a commented-out platform-level rate limiter (`express-rate-limit`; Nest's `@nestjs/throttler` is a guard, which does not run on Aventara's routes); left alone when the file already configures CORS, printed when it has no such line | you |
|
|
74
76
|
| `.env` (`DATABASE_URL`, and `CORS_ORIGINS="http://localhost:5173,http://localhost:3001"` appended unless a line sets it), `.gitignore` (`/src/generated/`, `/dev.db*`, `.env`) | you |
|
|
75
|
-
| `package.json
|
|
76
|
-
| `pnpm-workspace.yaml` (pnpm only)
|
|
77
|
-
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` |
|
|
77
|
+
| `package.json`: `@aventara/*` at this CLI's version, `prisma`, `@prisma/client` and the driver at `7.10.0`; `aventara:prepare`, run by `postinstall` and by `prebuild`, `prestart`, `prestart:dev` and `prestart:debug`, and the `.env` on the start scripts and `test:e2e` (`--env-file`, `--env-file-if-exists`) | you |
|
|
78
|
+
| `pnpm-workspace.yaml` (pnpm only): `allowBuilds` for Prisma's and SQLite's install scripts | you |
|
|
79
|
+
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` | generated by `aventara:prepare` (run on every install, build and start, so a schema change needs only a restart); never edit |
|
|
78
80
|
|
|
79
81
|
### Next steps it prints
|
|
80
82
|
|
|
@@ -111,23 +113,29 @@ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
|
|
|
111
113
|
Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
|
|
112
114
|
whenever the schema or the configuration does: fetch it again, and regenerate the client.
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
Resource keys are the model names, as written. `model User` is `User` everywhere: on the wire
|
|
117
|
+
(`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
|
|
118
|
+
lower-cased or pluralized.
|
|
119
|
+
|
|
120
|
+
Results are read-only. A list result is a `readonly` array, so type it `readonly User[]`:
|
|
121
|
+
`const users: readonly User[] = await avClient.User.find.many({});`.
|
|
119
122
|
|
|
120
123
|
## Supported
|
|
121
124
|
|
|
122
|
-
NestJS 12
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
- NestJS 12.
|
|
126
|
+
- Node `^22.18.0 || >=24.2.0`. On any other Node the bin refuses in one sentence naming that range. On Node 22 use
|
|
127
|
+
npm ≥ 11, because Node 22's bundled npm 10 cannot install Nest 12's own scaffold. A CommonJS project's jest e2e needs
|
|
128
|
+
Node ≥ 24.9, which is Nest's and Jest's limit.
|
|
129
|
+
- npm and pnpm. Yarn is not supported.
|
|
130
|
+
- ESM and CommonJS projects.
|
|
131
|
+
- Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL.
|
|
132
|
+
|
|
133
|
+
The ORMs, majors, databases and drivers on offer are generated from the Aventara adapter packages that exist
|
|
134
|
+
(`src/catalog/adapter.catalog.generated.ts`), never listed by hand, so a future `@aventara/prisma8-adapter` appears
|
|
135
|
+
once it exists.
|
|
129
136
|
|
|
130
137
|
## License
|
|
131
138
|
|
|
132
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
133
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
139
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
140
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
141
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export class
|
|
2
|
-
name = "
|
|
1
|
+
export class AvConflictsNotConfirmedError extends Error {
|
|
2
|
+
name = "AvConflictsNotConfirmedError";
|
|
3
3
|
}
|
|
4
4
|
export function conflictWarning(conflicts) {
|
|
5
5
|
return `${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content, and initializing will replace ${conflicts.length === 1 ? "it" : "them"}`;
|
|
@@ -10,7 +10,7 @@ export async function confirmConflicts(conflicts, consent) {
|
|
|
10
10
|
}
|
|
11
11
|
const them = conflicts.length === 1 ? "it" : "them";
|
|
12
12
|
if (!consent.prompter.interactive) {
|
|
13
|
-
throw new
|
|
13
|
+
throw new AvConflictsNotConfirmedError(`not initializing, and nothing was touched: ${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content and nobody can be asked (stdin is not a terminal); run \`aventara init --yes\` to replace ${them}`);
|
|
14
14
|
}
|
|
15
15
|
consent.prompter.say(`aventara: warning: ${conflictWarning(conflicts)}.`);
|
|
16
16
|
const answer = await consent.prompter.ask({
|
|
@@ -19,6 +19,6 @@ export async function confirmConflicts(conflicts, consent) {
|
|
|
19
19
|
defaultValue: "N",
|
|
20
20
|
});
|
|
21
21
|
if (!/^y(?:es)?$/i.test(answer.trim())) {
|
|
22
|
-
throw new
|
|
22
|
+
throw new AvConflictsNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}`);
|
|
23
23
|
}
|
|
24
24
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { AvConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
|
|
3
3
|
import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AvCliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
|
|
5
|
+
import { AvProjectRefusedError } from "./project/project.inspector.js";
|
|
6
6
|
import { runCommand } from "./run/command.runner.js";
|
|
7
|
-
import {
|
|
7
|
+
import { AvInstallFailedError, runInit } from "./run/init.orchestrator.js";
|
|
8
8
|
import { runNew } from "./run/new.orchestrator.js";
|
|
9
9
|
import { progressModeOf } from "./run/step.reporter.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { AvUnansweredQuestionsError, } from "./wizard/answer.resolver.js";
|
|
11
|
+
import { AvWizardCancelledError, createReadlinePrompter, } from "./wizard/readline.prompter.js";
|
|
12
|
+
import { AvWizardAnswerError } from "./wizard/wizard.questions.js";
|
|
13
13
|
const REFUSALS = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
AvCliCommandError,
|
|
15
|
+
AvWizardAnswerError,
|
|
16
|
+
AvUnansweredQuestionsError,
|
|
17
|
+
AvWizardCancelledError,
|
|
18
|
+
AvProjectRefusedError,
|
|
19
|
+
AvConflictsNotConfirmedError,
|
|
20
|
+
AvInstallFailedError,
|
|
21
21
|
];
|
|
22
22
|
const MANIFEST = new URL("../package.json", import.meta.url);
|
|
23
23
|
function ownVersion() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
2
|
+
export class AvCliCommandError extends Error {
|
|
3
|
+
name = "AvCliCommandError";
|
|
4
4
|
}
|
|
5
5
|
const OPTIONS = [
|
|
6
6
|
{
|
|
@@ -101,7 +101,7 @@ ${asked(catalog)}`;
|
|
|
101
101
|
export function parseCliCommand(argv) {
|
|
102
102
|
const [command, ...rest] = argv;
|
|
103
103
|
if (command === undefined) {
|
|
104
|
-
throw new
|
|
104
|
+
throw new AvCliCommandError(`a command is required: new or init; ${HELP}`);
|
|
105
105
|
}
|
|
106
106
|
if (command === "--help" || command === "-h") {
|
|
107
107
|
return { command: "help" };
|
|
@@ -110,7 +110,7 @@ export function parseCliCommand(argv) {
|
|
|
110
110
|
return { command: "version" };
|
|
111
111
|
}
|
|
112
112
|
if (command !== "new" && command !== "init") {
|
|
113
|
-
throw new
|
|
113
|
+
throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
|
|
114
114
|
}
|
|
115
115
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
116
116
|
return { command: "help", topic: command };
|
|
@@ -124,7 +124,7 @@ export function parseCliCommand(argv) {
|
|
|
124
124
|
const argument = rest[index];
|
|
125
125
|
if (!argument.startsWith("-")) {
|
|
126
126
|
if (positional === undefined || given[positional.id] !== undefined) {
|
|
127
|
-
throw new
|
|
127
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
|
|
128
128
|
}
|
|
129
129
|
given[positional.id] = argument;
|
|
130
130
|
continue;
|
|
@@ -137,20 +137,20 @@ export function parseCliCommand(argv) {
|
|
|
137
137
|
const canonical = question?.flag ?? option?.flag ?? flag;
|
|
138
138
|
if (question === undefined && option === undefined) {
|
|
139
139
|
const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
|
|
140
|
-
throw new
|
|
140
|
+
throw new AvCliCommandError(elsewhere.length === 0
|
|
141
141
|
? `unknown flag ${flag}; ${HELP}`
|
|
142
142
|
: `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
143
143
|
}
|
|
144
144
|
if (option !== undefined && !option.commands.includes(command)) {
|
|
145
|
-
throw new
|
|
145
|
+
throw new AvCliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
146
146
|
}
|
|
147
147
|
if (seen.has(canonical)) {
|
|
148
|
-
throw new
|
|
148
|
+
throw new AvCliCommandError(`${canonical} was given twice; ${HELP}`);
|
|
149
149
|
}
|
|
150
150
|
seen.add(canonical);
|
|
151
151
|
if (question === undefined) {
|
|
152
152
|
if (inline !== undefined) {
|
|
153
|
-
throw new
|
|
153
|
+
throw new AvCliCommandError(`${canonical} takes no value; ${HELP}`);
|
|
154
154
|
}
|
|
155
155
|
options[option.key] = true;
|
|
156
156
|
continue;
|
|
@@ -158,7 +158,7 @@ export function parseCliCommand(argv) {
|
|
|
158
158
|
const value = inline ?? rest[index + 1];
|
|
159
159
|
if (value === undefined ||
|
|
160
160
|
(inline === undefined && value.startsWith("-"))) {
|
|
161
|
-
throw new
|
|
161
|
+
throw new AvCliCommandError(`${canonical} takes a value; ${HELP}`);
|
|
162
162
|
}
|
|
163
163
|
if (inline === undefined) {
|
|
164
164
|
index += 1;
|
|
@@ -3,7 +3,7 @@ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
|
|
|
3
3
|
import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
|
|
4
4
|
import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
|
|
5
5
|
import { APP_MODULE, } from "../project/project.inspector.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SCAFFOLD_API, } from "../templates/template.registry.js";
|
|
7
7
|
export const PREPARE_SCRIPT = "aventara:prepare";
|
|
8
8
|
const PREPARED_SCRIPTS = [
|
|
9
9
|
"build",
|
|
@@ -113,9 +113,7 @@ export function planInit(input) {
|
|
|
113
113
|
keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
114
114
|
replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
115
115
|
const spec = input.read(E2E_SPEC);
|
|
116
|
-
const tested = spec === undefined
|
|
117
|
-
? undefined
|
|
118
|
-
: spliceContractTest(spec, SCAFFOLD_ENTRYPOINT);
|
|
116
|
+
const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
|
|
119
117
|
if (tested !== undefined) {
|
|
120
118
|
keeping.push(...writeOf(E2E_SPEC, spec, tested));
|
|
121
119
|
replacing.push(...writeOf(E2E_SPEC, spec, tested));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lowestMajorOf } from "../catalog/range.reader.js";
|
|
4
|
-
export class
|
|
5
|
-
name = "
|
|
4
|
+
export class AvProjectRefusedError extends Error {
|
|
5
|
+
name = "AvProjectRefusedError";
|
|
6
6
|
}
|
|
7
7
|
const NEST_MAJOR = 12;
|
|
8
8
|
const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
|
|
@@ -23,32 +23,32 @@ function matchesFamily(pattern, name) {
|
|
|
23
23
|
export function inspectProject(directory, catalog) {
|
|
24
24
|
const manifestPath = path.join(directory, "package.json");
|
|
25
25
|
if (!existsSync(manifestPath)) {
|
|
26
|
-
throw new
|
|
26
|
+
throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
|
|
27
27
|
}
|
|
28
28
|
const manifestText = readFileSync(manifestPath, "utf8");
|
|
29
29
|
const manifest = JSON.parse(manifestText);
|
|
30
30
|
const nest = dependencyRange(manifest, "@nestjs/core");
|
|
31
31
|
if (nest === undefined) {
|
|
32
|
-
throw new
|
|
32
|
+
throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
|
|
33
33
|
}
|
|
34
34
|
if (lowestMajorOf(nest) !== NEST_MAJOR) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new AvProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
|
|
36
36
|
}
|
|
37
37
|
const typescript = dependencyRange(manifest, "typescript");
|
|
38
38
|
const typescriptMajor = typescript === undefined ? undefined : lowestMajorOf(typescript);
|
|
39
39
|
if (typescriptMajor !== undefined &&
|
|
40
40
|
typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
|
|
41
|
-
throw new
|
|
41
|
+
throw new AvProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 6 or 5`);
|
|
42
42
|
}
|
|
43
43
|
const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
|
|
44
44
|
const known = lockfiles.filter((manager) => manager !== "yarn");
|
|
45
45
|
if (lockfiles.includes("yarn") && known.length === 0) {
|
|
46
|
-
throw new
|
|
46
|
+
throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
|
|
47
47
|
}
|
|
48
48
|
const appModule = path.join(directory, APP_MODULE);
|
|
49
49
|
if (existsSync(appModule) &&
|
|
50
50
|
/\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
|
|
51
|
-
throw new
|
|
51
|
+
throw new AvProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
|
|
52
52
|
}
|
|
53
53
|
const dependencies = [
|
|
54
54
|
...Object.keys(manifest.dependencies ?? {}),
|
|
@@ -4,7 +4,7 @@ import { confirmConflicts } from "../apply/conflict.confirmer.js";
|
|
|
4
4
|
import { matchInstalledOrm, matchOrmSetup, } from "../catalog/catalog.matcher.js";
|
|
5
5
|
import { planInit } from "../plan/project.planner.js";
|
|
6
6
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
7
|
-
import {
|
|
7
|
+
import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
|
|
8
8
|
import { scanServices } from "../project/service.detector.js";
|
|
9
9
|
import { templatesFor, } from "../templates/template.registry.js";
|
|
10
10
|
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
@@ -13,8 +13,8 @@ import { createStepReporter, } from "./step.reporter.js";
|
|
|
13
13
|
export function printNextSteps(stdout, steps) {
|
|
14
14
|
stdout(`Next:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
|
|
15
15
|
}
|
|
16
|
-
export class
|
|
17
|
-
name = "
|
|
16
|
+
export class AvInstallFailedError extends Error {
|
|
17
|
+
name = "AvInstallFailedError";
|
|
18
18
|
}
|
|
19
19
|
export async function runInit(command, io) {
|
|
20
20
|
const { progress, nextSteps } = await initProject(command, io);
|
|
@@ -88,7 +88,7 @@ export async function initProject(command, io) {
|
|
|
88
88
|
onOutput: progress.output,
|
|
89
89
|
});
|
|
90
90
|
if (installed.code !== 0) {
|
|
91
|
-
throw new
|
|
91
|
+
throw new AvInstallFailedError(`\`${packageManager} install\` exited with code ${installed.code} after the files were written; fix what it reports and run \`${packageManager} install\` again:\n${installed.stderr.trimEnd()}`);
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
}
|
|
@@ -144,25 +144,25 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
144
144
|
installCommand: `${detectedPackageManager} install`,
|
|
145
145
|
});
|
|
146
146
|
if (verdict.kind === "refused") {
|
|
147
|
-
throw new
|
|
147
|
+
throw new AvProjectRefusedError(verdict.sentence);
|
|
148
148
|
}
|
|
149
149
|
const { entry } = verdict;
|
|
150
150
|
const templates = templatesFor(entry);
|
|
151
151
|
const facts = templates.readProject(io.directory, inspection.manifest);
|
|
152
152
|
const problem = matchOrmSetup(entry, facts);
|
|
153
153
|
if (problem !== undefined) {
|
|
154
|
-
throw new
|
|
154
|
+
throw new AvProjectRefusedError(problem);
|
|
155
155
|
}
|
|
156
156
|
const provider = facts.provider;
|
|
157
157
|
if (facts.clientOutput === undefined) {
|
|
158
|
-
throw new
|
|
158
|
+
throw new AvProjectRefusedError(`this project's ${facts.generator} generator block in ${facts.schema} has no output, and the generate step reads the client from it; give it one, such as ../src/generated/${entry.orm.family}`);
|
|
159
159
|
}
|
|
160
160
|
for (const [flag, given, found] of [
|
|
161
161
|
["--orm", command.given.orm, entry.alias],
|
|
162
162
|
["--db", command.given.db, provider],
|
|
163
163
|
]) {
|
|
164
164
|
if (given !== undefined && given.trim().toLowerCase() !== found) {
|
|
165
|
-
throw new
|
|
165
|
+
throw new AvProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
const scan = scanServices(io.directory, templates.clientShape(facts));
|
|
@@ -2,10 +2,10 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
5
|
-
import {
|
|
5
|
+
import { AvProjectRefusedError } from "../project/project.inspector.js";
|
|
6
6
|
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
7
7
|
import { NEW_QUESTIONS } from "../wizard/wizard.questions.js";
|
|
8
|
-
import {
|
|
8
|
+
import { AvInstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
|
|
9
9
|
import { commitScaffold, gitSurroundings } from "./scaffold.committer.js";
|
|
10
10
|
import { createStepReporter } from "./step.reporter.js";
|
|
11
11
|
export const NEST_CLI = "@nestjs/cli@12.0.8";
|
|
@@ -34,7 +34,7 @@ export async function runNew(command, io) {
|
|
|
34
34
|
}
|
|
35
35
|
const directory = path.join(io.cwd, name);
|
|
36
36
|
if (existsSync(directory) && readdirSync(directory).length > 0) {
|
|
37
|
-
throw new
|
|
37
|
+
throw new AvProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
|
|
38
38
|
}
|
|
39
39
|
const surroundings = command.skipGit
|
|
40
40
|
? undefined
|
|
@@ -63,7 +63,7 @@ export async function runNew(command, io) {
|
|
|
63
63
|
onOutput: progress.output,
|
|
64
64
|
});
|
|
65
65
|
if (created.code !== 0) {
|
|
66
|
-
throw new
|
|
66
|
+
throw new AvInstallFailedError(`\`${runner} ${args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again:\n${created.stderr.trimEnd()}`);
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
const appModule = path.join(directory, "src", "app.module.ts");
|
|
@@ -71,7 +71,7 @@ export async function runNew(command, io) {
|
|
|
71
71
|
? createHash("sha256").update(readFileSync(appModule)).digest("hex")
|
|
72
72
|
: "(absent)";
|
|
73
73
|
if (digest !== NEST_APP_MODULE_SHA256) {
|
|
74
|
-
throw new
|
|
74
|
+
throw new AvInstallFailedError(`nest new's output changed: ${appModule} is not what ${NEST_CLI} writes (sha256 ${digest}), so nothing of Aventara's was written over it; run aventara init inside ${directory} to add Aventara to it`);
|
|
75
75
|
}
|
|
76
76
|
const { nextSteps } = await initProject({
|
|
77
77
|
command: "init",
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export const DOCS_BASE_URL = "https://aventara-
|
|
1
|
+
export const DOCS_BASE_URL = "https://aventara-framework.vercel.app";
|
|
2
|
+
export const EXPOSURE_DOCS = `${DOCS_BASE_URL}/docs/exposure`;
|
|
2
3
|
export const RATE_LIMITING_DOCS = `${DOCS_BASE_URL}/docs/limits-and-safety#rate-limiting`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lowestVersionOf } from "../../catalog/range.reader.js";
|
|
4
|
-
import {
|
|
4
|
+
import { EXPOSURE_DOCS } from "../docs.links.js";
|
|
5
|
+
import { SCAFFOLD_API, } from "../template.registry.js";
|
|
5
6
|
const CLIENT_OUTPUT = "src/generated/prisma";
|
|
6
7
|
const SCHEMA = "prisma/schema.prisma";
|
|
7
8
|
const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
|
|
@@ -111,18 +112,38 @@ function aventaraConfig(input) {
|
|
|
111
112
|
const file = "src/aventara.config.ts";
|
|
112
113
|
const service = input.service.exportName;
|
|
113
114
|
return [
|
|
115
|
+
"import type { AvDiagnostic } from '@aventara/core';",
|
|
114
116
|
`import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
|
|
117
|
+
"import { Logger } from '@nestjs/common';",
|
|
115
118
|
`import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
|
|
116
119
|
`import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
|
|
117
120
|
"",
|
|
121
|
+
"const logger = new Logger('Aventara');",
|
|
122
|
+
"",
|
|
118
123
|
"/**",
|
|
119
|
-
" * Aventara's configuration: the
|
|
120
|
-
" *
|
|
121
|
-
" *
|
|
124
|
+
" * Aventara's configuration: where the API is mounted (`api`), and what you add",
|
|
125
|
+
" * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
|
|
126
|
+
" * `resources` tree).",
|
|
127
|
+
" * AventaraModule (app.module.ts) builds the framework from it; `discovery` is",
|
|
128
|
+
" * regenerated by the aventara:prepare script.",
|
|
122
129
|
" */",
|
|
123
130
|
`export async function aventaraConfig(prisma: ${service}) {`,
|
|
124
131
|
" return {",
|
|
125
|
-
`
|
|
132
|
+
` api: '${SCAFFOLD_API}',`,
|
|
133
|
+
" // Everything the adapter offers is exposed. List what to hide under",
|
|
134
|
+
" // `exemptions`, or set `exposed: false` and list what to expose instead.",
|
|
135
|
+
` // See: ${EXPOSURE_DOCS}`,
|
|
136
|
+
" exposed: true,",
|
|
137
|
+
" // When something fails inside the server, the caller gets a plain 500 and",
|
|
138
|
+
" // Aventara logs one line naming the request. This logs the error itself,",
|
|
139
|
+
" // with its stack, through Nest's logger.",
|
|
140
|
+
" diagnostics: (diagnostic: AvDiagnostic) => {",
|
|
141
|
+
" const { error } = diagnostic;",
|
|
142
|
+
" logger.error(",
|
|
143
|
+
` \`\${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
|
|
144
|
+
" error instanceof Error ? error.stack : String(error),",
|
|
145
|
+
" );",
|
|
146
|
+
" },",
|
|
126
147
|
" adapter: await createPrismaAdapter({",
|
|
127
148
|
input.service.clientMember === undefined
|
|
128
149
|
? " client: prisma,"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
1
|
+
import { AvWizardAnswerError } from "./wizard.questions.js";
|
|
2
|
+
export class AvUnansweredQuestionsError extends Error {
|
|
3
|
+
name = "AvUnansweredQuestionsError";
|
|
4
4
|
}
|
|
5
5
|
function flagSpelling(question, context) {
|
|
6
6
|
if (!question.flag.startsWith("--")) {
|
|
@@ -41,7 +41,7 @@ async function prompt(question, context, prompter) {
|
|
|
41
41
|
return question.parse(raw, context);
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
|
-
if (!(error instanceof
|
|
44
|
+
if (!(error instanceof AvWizardAnswerError)) {
|
|
45
45
|
throw error;
|
|
46
46
|
}
|
|
47
47
|
prompter.say(`${error.message}.`);
|
|
@@ -90,7 +90,7 @@ export async function resolveAnswers(questions, sources) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
if (missing.length > 0) {
|
|
93
|
-
throw new
|
|
93
|
+
throw new AvUnansweredQuestionsError(unansweredSentence(missing, sources.yes));
|
|
94
94
|
}
|
|
95
95
|
return answered;
|
|
96
96
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
2
|
+
export class AvWizardCancelledError extends Error {
|
|
3
|
+
name = "AvWizardCancelledError";
|
|
4
4
|
}
|
|
5
5
|
export function createReadlinePrompter(streams) {
|
|
6
6
|
let session;
|
|
7
7
|
let ended = false;
|
|
8
8
|
const lines = [];
|
|
9
9
|
const waiting = [];
|
|
10
|
-
const cancelled = () => new
|
|
10
|
+
const cancelled = () => new AvWizardCancelledError("the wizard was closed before it was answered");
|
|
11
11
|
const open = () => {
|
|
12
12
|
if (session === undefined) {
|
|
13
13
|
session = createInterface({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { modulesProviding, } from "../project/service.detector.js";
|
|
2
2
|
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
3
|
-
export class
|
|
4
|
-
name = "
|
|
3
|
+
export class AvWizardAnswerError extends Error {
|
|
4
|
+
name = "AvWizardAnswerError";
|
|
5
5
|
}
|
|
6
6
|
function ormLabel(entry) {
|
|
7
7
|
return `${entry.orm.label} ${entry.orm.major}`;
|
|
@@ -32,7 +32,7 @@ export const NAME_QUESTION = {
|
|
|
32
32
|
parse: (raw) => {
|
|
33
33
|
const name = raw.trim();
|
|
34
34
|
if (!NAME.test(name)) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a project name: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
|
|
36
36
|
}
|
|
37
37
|
return name;
|
|
38
38
|
},
|
|
@@ -52,7 +52,7 @@ export const ORM_QUESTION = {
|
|
|
52
52
|
const alias = raw.trim().toLowerCase();
|
|
53
53
|
const entry = context.catalog.find((candidate) => candidate.alias === alias);
|
|
54
54
|
if (entry === undefined) {
|
|
55
|
-
throw new
|
|
55
|
+
throw new AvWizardAnswerError(`there is no Aventara adapter for the ORM ${JSON.stringify(raw)}; ${ormAliasesSentence(context.catalog)}`);
|
|
56
56
|
}
|
|
57
57
|
return entry;
|
|
58
58
|
},
|
|
@@ -82,7 +82,7 @@ export const DB_QUESTION = {
|
|
|
82
82
|
const orm = requireOrm(context);
|
|
83
83
|
const db = raw.trim().toLowerCase();
|
|
84
84
|
if (!orm.providers.includes(db)) {
|
|
85
|
-
throw new
|
|
85
|
+
throw new AvWizardAnswerError(`the ${ormLabel(orm)} adapter does not support the database ${JSON.stringify(raw)}; choose ${orm.providers.join(" or ")}`);
|
|
86
86
|
}
|
|
87
87
|
return db;
|
|
88
88
|
},
|
|
@@ -98,7 +98,7 @@ export const PACKAGE_MANAGER_QUESTION = {
|
|
|
98
98
|
parse: (raw) => {
|
|
99
99
|
const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim().toLowerCase());
|
|
100
100
|
if (manager === undefined) {
|
|
101
|
-
throw new
|
|
101
|
+
throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}`);
|
|
102
102
|
}
|
|
103
103
|
return manager;
|
|
104
104
|
},
|
|
@@ -109,7 +109,7 @@ function reference(raw, flag) {
|
|
|
109
109
|
const exportName = raw.slice(hash + 1).trim();
|
|
110
110
|
const file = raw.slice(0, hash).trim().replace(/^\.\//, "");
|
|
111
111
|
if (hash <= 0 || !/^[A-Za-z_$][\w$]*$/.test(exportName)) {
|
|
112
|
-
throw new
|
|
112
|
+
throw new AvWizardAnswerError(`${flag} takes <path#Export>, a file and the name it exports, such as src/prisma.service.ts#PrismaService; got ${JSON.stringify(raw)}`);
|
|
113
113
|
}
|
|
114
114
|
return { file, exportName };
|
|
115
115
|
}
|
|
@@ -166,7 +166,7 @@ export const SERVICE_QUESTION = {
|
|
|
166
166
|
const options = serviceOptions(services);
|
|
167
167
|
const chosen = options.find((option) => option.file === wanted.file && option.exportName === wanted.exportName);
|
|
168
168
|
if (chosen === undefined) {
|
|
169
|
-
throw new
|
|
169
|
+
throw new AvWizardAnswerError(`${spelled(wanted)} is not a service found in src/ (a class extending ${services.clientClass}, or one holding it); found: ${options
|
|
170
170
|
.filter((option) => !option.isNew)
|
|
171
171
|
.map(spelled)
|
|
172
172
|
.join(", ") || "none"}`);
|
|
@@ -202,7 +202,7 @@ export const MODULE_QUESTION = {
|
|
|
202
202
|
: modulesProviding(services.scan, service);
|
|
203
203
|
const chosen = modules.find((module) => module.file === wanted.file && module.exportName === wanted.exportName);
|
|
204
204
|
if (chosen === undefined) {
|
|
205
|
-
throw new
|
|
205
|
+
throw new AvWizardAnswerError(`${spelled(wanted)} is not a module that provides and exports ${service.exportName}; found: ${modules.map(spelled).join(", ") || "none"}`);
|
|
206
206
|
}
|
|
207
207
|
return { file: chosen.file, exportName: chosen.exportName };
|
|
208
208
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/cli",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Scaffolds an Aventara server: `aventara new` for a new NestJS project, `aventara init` for an existing one.",
|
|
6
6
|
"type": "module",
|