@appxdigital/appx-core 0.1.121-beta.4 → 0.1.121-beta.6

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 ADDED
@@ -0,0 +1,104 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@appxdigital/appx-core` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
7
+
8
+ ## How to read this file
9
+
10
+ - **This file is the collapsed, production changelog** — one section per version published to the `latest` dist-tag. Granular per-build notes for `alpha`/`beta` prereleases live in [`PRERELEASES.md`](./PRERELEASES.md); the release process is documented in [`RELEASING.md`](./RELEASING.md).
11
+ - **Every released version has a section.** Newest first.
12
+ - **Changes that require action in an existing project are listed under `### Migration`** — copy-paste ready, small enough to hand to an AI coding agent. Items outside that block are drop-in.
13
+ - **Scaffold-only changes** (files under `cli/scaffold/`) affect *new* projects created with `appx-core create`. Existing projects own their copies of those files and apply the migration by hand.
14
+
15
+ ---
16
+
17
+ ## [0.1.121] — 2026-07-14
18
+
19
+ Hardening release: request validation, boot configuration, generated-CRUD DTOs, and data-access create enforcement.
20
+
21
+ ### Added
22
+
23
+ - **`setupCoreSecurity(app, options?)`** — one call that applies baseline HTTP hardening: a global `ValidationPipe` (`{ transform: true, whitelist: true, forbidNonWhitelisted: true }`), CORS (`cors` option, default `{ origin: 'http://localhost:3000', credentials: true }`, throws if `origin: '*'` is combined with `credentials: true`), and **Helmet** security headers (on by default; `helmet` option to tune/disable). `helmet` is a new peer dependency.
24
+ - **`buildCoreSessionOptions({ secret, store, cookieName?, ttlSeconds?, secure?, sameSite? })`** — builds hardened `express-session` options. Throws if `secret` is missing or `< 32` chars (no fallback); sets `cookie: { httpOnly, secure (prod), sameSite: 'lax', maxAge }`.
25
+ - **`coreEnvFilePath()`** — returns `['.env.${NODE_ENV}', '.env']` (env-specific file wins, plain `.env` is a fallback) and throws if `NODE_ENV` is unset.
26
+ - **Per-model DTO generation** (`appx generate`). Each model+action gets a `class-validator` DTO so generated controllers type their request body concretely and the whitelist pipe can strip unknown fields. Two files:
27
+ - `src/generated/dto/<model>/<action>-<model>.generated.dto.ts` — base class, **always overwritten**, under the gitignored `src/generated/`. Never hand-edit.
28
+ - `src/modules/<model>/dto/<action>-<model>.dto.ts` — a hand-owned subclass, generated **once**, for custom validation. The controller imports this.
29
+ - **`/// @NoWrite` field annotation** — excludes a field from the generated create/update DTOs (all roles). Fields annotated `/// @Role(none)` are also treated as non-writable.
30
+ - **Create-permission enforcement in the data-access proxy.** `create` / `createMany` validate that the incoming data satisfies the permission's `conditions` before insert (own-scalar fields against the data; relation conditions by looking up the referenced parent), and default-deny when the model/role has no `create` permission. A development-only warning fires when a created row would not satisfy the model's `find` conditions.
31
+ - **`restrictedFields` on `create`** in `CoreController` (previously only `update`, which also read the wrong permission key so it never applied under `updateMany`).
32
+
33
+ ### Writable-field policy (generated DTOs)
34
+
35
+ A scalar field is included unless it is: the primary key (`@id`), a server-managed timestamp (`@default(now())` / `@updatedAt`), `/// @NoWrite`, or `/// @Role(none)`. Relation navigation fields are excluded; their scalar foreign keys are kept. Enums map to `@IsEnum`, scalars to the matching `@IsString`/`@IsInt`/`@IsBoolean`/`@IsNumber`/`@IsDateString`; `Json`/`Bytes` use `@Allow()`.
36
+
37
+ ### Changed (scaffold — new projects only)
38
+
39
+ - `main.ts` calls `setupCoreSecurity(app, { cors: { origin: corsOrigin, credentials: true } })`, builds session options via `buildCoreSessionOptions`, and reads `CORS_ORIGIN` from config.
40
+ - `app.module.ts` uses `coreEnvFilePath()` for `envFilePath`.
41
+ - Generated `package.json` adds `helmet`; the `start` script sets `NODE_ENV=development`.
42
+ - `AuthController.getAllSessions` / `closeSpecificSession` use `@UseGuards(AuthenticatedGuard)`.
43
+
44
+ ### Migration
45
+
46
+ > For existing projects. All edits are to files you own (`main.ts` / `app.module.ts` / `package.json` / your generated controllers). Safe to hand to an AI coding agent.
47
+
48
+ **1. Request validation & HTTP hardening — `src/main.ts`.**
49
+ ```ts
50
+ import { setupCoreSecurity } from '@appxdigital/appx-core';
51
+ // remove: app.useGlobalPipes(new ValidationPipe({ transform: true }));
52
+ // remove: app.enableCors({ origin: 'http://localhost:3000', credentials: true });
53
+ const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:3000';
54
+ setupCoreSecurity(app, { cors: { origin: corsOrigin, credentials: true } });
55
+ ```
56
+ - Install Helmet: `npm install helmet@^8.0.0` (applied automatically by `setupCoreSecurity`; tune with `{ helmet: {...} }`, disable with `{ helmet: false }`).
57
+ - If you register a `ValidationPipe` with custom options, pass them through: `setupCoreSecurity(app, { validationPipe: { /* overrides */ } })`.
58
+ - With `whitelist` + `forbidNonWhitelisted`, body properties not declared on a `class-validator` DTO are rejected (`400`). Confirm every DTO declares every field it legitimately accepts before deploying.
59
+ - Set `CORS_ORIGIN` to your real front-end origin in production. Do not combine `'*'` with credentials — the helper throws at boot. For a public credential-less API pass `{ cors: { origin: '*', credentials: false } }`.
60
+
61
+ **2. Session secret & cookie flags — `src/main.ts`.**
62
+ ```ts
63
+ import { buildCoreSessionOptions } from '@appxdigital/appx-core';
64
+ // replace your inline session({ secret: … }) (drop any hardcoded fallback secret) with:
65
+ app.use(session(buildCoreSessionOptions({
66
+ secret: configService.get<string>('SESSION_SECRET'),
67
+ cookieName: configService.get<string>('SESSION_COOKIE_NAME'),
68
+ ttlSeconds: configService.get<number>('SESSION_TTL') || 86400,
69
+ store: new CorePrismaSessionStore(prismaService, { ttl: sessionTTL }),
70
+ })));
71
+ ```
72
+ Ensure `SESSION_SECRET` is set (≥ 32 chars) in every environment — the app now refuses to boot without it.
73
+
74
+ **3. Environment file & `NODE_ENV` — `src/app.module.ts`.**
75
+ ```ts
76
+ import { coreEnvFilePath } from '@appxdigital/appx-core';
77
+ // ConfigModule.forRoot({ … envFilePath: coreEnvFilePath() })
78
+ ```
79
+ Set `NODE_ENV` explicitly (`production` / `development`) in every deploy and start script — boot fails if it is unset. If you use a bare `nest start`, change it to `cross-env NODE_ENV=development nest start`. (`coreEnvFilePath()` loads `.env.<NODE_ENV>` with `.env` as a fallback, so a plain `.env` still works.)
80
+
81
+ **4. Generated-CRUD DTOs.**
82
+ ```bash
83
+ npm install @appxdigital/appx-core@^0.1.121
84
+ npx appx generate
85
+ ```
86
+ Then **add the DTO-typed overrides to each existing generated controller** (generated once, not overwritten):
87
+ ```ts
88
+ import { Body, Param, Post, Put } from '@nestjs/common';
89
+ import { Permission } from '@appxdigital/appx-core';
90
+ import { Create<Model>Dto } from './dto/create-<model-kebab>.dto';
91
+ import { Update<Model>Dto } from './dto/update-<model-kebab>.dto';
92
+
93
+ @Post() @Permission('create')
94
+ async create(@Body() data: Create<Model>Dto) { return super.create(data as any); }
95
+
96
+ @Put(':id') @Permission('updateMany')
97
+ async update(@Param('id') id: string, @Body() data: Update<Model>Dto) { return super.update(id, data as any); }
98
+ ```
99
+ (Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate.
100
+
101
+ **5. Create-permission enforcement (behaviour change — review before upgrading).**
102
+ - Ensure every model/role that legitimately creates records has an explicit `create` permission (or `'ALL'`). Without one, create now returns `403` (previously unchecked). Internal framework flows (registration, sessions, tokens) are unaffected.
103
+ - If you supplied arbitrary values for a field a `create` condition constrains (e.g. an owner id), those requests now return `403` unless the value matches — use `setUserIdField` to have the framework set it.
104
+ - **`restrictedFields` under `updateMany` now applies** (it was a no-op before). Fields listed there are stripped on `PUT`. Remove any field that should remain writable.
package/PRERELEASES.md ADDED
@@ -0,0 +1,72 @@
1
+ # Prerelease log
2
+
3
+ Granular notes for **alpha / beta** builds of both packages —
4
+ `@appxdigital/appx-core` (library) and `@appxdigital/appx-core-cli` (CLI) —
5
+ newest first. Entries are labelled by package. These are the builds you install
6
+ to test a change in a real project before it ships:
7
+
8
+ ```bash
9
+ npm install @appxdigital/appx-core@beta # library, latest beta
10
+ npm install @appxdigital/appx-core@0.1.121-beta.0 # a specific library build
11
+ npx @appxdigital/appx-core-cli@beta create # CLI, latest beta
12
+ ```
13
+
14
+ Each prerelease gets its own entry here with fine-grained notes. When a version
15
+ is promoted to production it is **collapsed** into a single curated section in
16
+ [`CHANGELOG.md`](./CHANGELOG.md) with aggregated changes and consolidated
17
+ migration steps — that public changelog is the one an upgrading project reads.
18
+ Nothing here is a substitute for it.
19
+
20
+ Prereleases are excluded from normal semver range resolution, so a project on
21
+ `^0.1.x` never pulls one by accident — you always opt in via the `@beta` /
22
+ `@alpha` tag or an exact version.
23
+
24
+ ---
25
+
26
+ ## [0.1.121-beta.6] — unreleased
27
+
28
+ **Packaging.** The npm tarball now ships `CHANGELOG.md`, `PRERELEASES.md`, and
29
+ `RELEASING.md` (previously only `dist/` + `README.md`). Migration steps are now
30
+ readable from inside an installed `node_modules/@appxdigital/appx-core`, so an
31
+ upgrading project (or an AI agent) can apply them without cloning the repo. No
32
+ code change.
33
+
34
+ ## [0.1.121-beta.5] — unreleased
35
+
36
+ **Library.** Field-level access control (`/// @Role(...)`) now applies to
37
+ **nested** relation selections, not just top-level fields. Previously a query
38
+ that selected a restricted field *through* a relation
39
+ (`select: { relation: { select: { restrictedField: true } } }`) received that
40
+ field; the data-access proxy now omits it at every depth, for both `select` and
41
+ `include`, while preserving any `where` / `orderBy` you set on the relation.
42
+
43
+ - Exercise reads that pull fields through relations (nested `select` / `include`,
44
+ including multi-level) and confirm the values you expect still come back — a
45
+ field annotated `@Role(<roles>)` should be absent for roles not listed.
46
+ - No API change; regenerate is not required.
47
+
48
+ ## [0.1.121-beta.0] — unreleased
49
+
50
+ First prerelease of the `0.1.121` hardening set. Testing target before this is
51
+ promoted to `latest`. Full aggregated notes and migration live under
52
+ `[0.1.121]` in `CHANGELOG.md`; the highlights to exercise in a project:
53
+
54
+ - **`setupCoreSecurity(app, options?)`** — one call for the global
55
+ `ValidationPipe` (whitelist + `forbidNonWhitelisted`), CORS, and Helmet.
56
+ Verify existing DTOs still accept every field they legitimately take (the
57
+ whitelist rejects undeclared body properties with `400`).
58
+ - **`buildCoreSessionOptions(...)`** — requires a session secret ≥ 32 chars and
59
+ sets hardened cookie flags. Confirm your env provides the secret.
60
+ - **`coreEnvFilePath()`** — `['.env.${NODE_ENV}', '.env']`; throws if
61
+ `NODE_ENV` is unset. Confirm boot scripts set `NODE_ENV`.
62
+ - **Per-model generated DTOs** — regenerate (`appx generate`) and check the
63
+ generated controllers compile and accept your writable fields.
64
+ - **Create-permission enforcement in the data-access proxy** — `create` /
65
+ `createMany` now validate incoming data against the permission conditions and
66
+ default-deny when a model/role has no `create` permission. Exercise every
67
+ create path in the consuming app and confirm intended creates still succeed;
68
+ set explicit `create` permissions (or `ALL`) where needed.
69
+
70
+ > How to try it: bump this package to `0.1.121-beta.0` in the project under
71
+ > test (`npm install @appxdigital/appx-core@beta`), run the app's own suite,
72
+ > and smoke-test create/update flows and boot. Report back before promotion.
package/README.md CHANGED
@@ -4,6 +4,10 @@
4
4
  <img src="./public/appx-core-logo.png" alt="AppX Core Logo" width="220" />
5
5
  </p>
6
6
 
7
+ [![CI](https://github.com/appxdigital/appx-core/actions/workflows/ci.yml/badge.svg)](https://github.com/appxdigital/appx-core/actions/workflows/ci.yml)
8
+ [![Release](https://github.com/appxdigital/appx-core/actions/workflows/release.yml/badge.svg)](https://github.com/appxdigital/appx-core/actions/workflows/release.yml)
9
+ [![Release CLI](https://github.com/appxdigital/appx-core/actions/workflows/release-cli.yml/badge.svg)](https://github.com/appxdigital/appx-core/actions/workflows/release-cli.yml)
10
+
7
11
  **AppX Core** is a production-oriented backend foundation that accelerates the delivery of secure, data-access-controlled APIs with an integrated admin backoffice. It scaffolds a native **NestJS** application and extends it with opinionated building blocks for authentication, authorization (ABAC-first), CRUD acceleration, and administration through **AdminJS**.
8
12
 
9
13
  Built and maintained by **AppX** (appx-digital.com).
@@ -33,6 +37,7 @@ Built and maintained by **AppX** (appx-digital.com).
33
37
  - [Permissions](#permissions)
34
38
  - [Permissions Configuration Shape](#permissions-configuration-shape)
35
39
  - [Rules](#rules)
40
+ - [Reading Related Models](#reading-related-models)
36
41
  - [Action Aliases and Fallbacks](#action-aliases-and-fallbacks)
37
42
  - [Custom Controller Actions](#custom-controller-actions)
38
43
  - [Temporarily Exposing Models](#temporarily-exposing-models)
@@ -110,6 +115,21 @@ AppX Core is designed as a **base layer** on top of NestJS:
110
115
  npm install -g @appxdigital/appx-core-cli
111
116
  ```
112
117
 
118
+ **Release channels.** AppX Core publishes both packages on two npm dist-tags:
119
+
120
+ - **`latest`** — stable releases. This is what the commands above install.
121
+ - **`beta`** — prereleases, for trying upcoming changes before they're promoted. Opt in explicitly with the `@beta` tag (a caret range such as `^0.1.x` never resolves to a prerelease on its own):
122
+
123
+ ```bash
124
+ npm install -g @appxdigital/appx-core-cli@beta # CLI prerelease
125
+ ```
126
+
127
+ `appx-core create` scaffolds a project that depends on the **stable** library. To test a library prerelease in that project, install it there:
128
+
129
+ ```bash
130
+ npm install @appxdigital/appx-core@beta # inside your project
131
+ ```
132
+
113
133
  ### 2) Create a New Project
114
134
 
115
135
  ```bash
@@ -119,10 +139,13 @@ cd <your-project>
119
139
 
120
140
  ### 3) Configure Environment
121
141
 
122
- Create a `.env` file at the project root:
142
+ `appx-core create` **already writes a `.env`** at the project root, with secure
143
+ random secrets generated for you. You don't need to create it by hand — just
144
+ open it and set your database connection (and `CORS_ORIGIN` for production). The
145
+ generated file looks like this:
123
146
 
124
147
  ```bash
125
- ## DataBase Configurations ##
148
+ ## Database ##
126
149
  DB_HOST=127.0.0.1
127
150
  DB_PORT=3306
128
151
  DB_USER=root
@@ -131,22 +154,36 @@ DB_NAME=generic
131
154
  DB_PROVIDER=mysql
132
155
  DB_URL="${DB_PROVIDER}://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
133
156
 
134
- ## Project configurations ##
157
+ ## App ##
135
158
  APP_PORT=3000
136
159
  USE_TRANSACTION=true
160
+ CORS_ORIGIN=http://localhost:3000 # your front-end origin; set to your real domain in production
137
161
 
138
162
  ## Sessions ##
139
- SESSION_SECRET="change-me"
140
- SESSION_COOKIE_NAME="session_nestjs-project"
163
+ SESSION_SECRET=<64-char random hex — generated for you>
164
+ SESSION_COOKIE_NAME=session_nestjs-project
141
165
  SESSION_TTL=86400
142
166
 
143
167
  ## JWT ##
144
168
  JWT_EXPIRES_IN=10d
145
169
  JWT_REFRESH_EXPIRES_IN=1y
146
- JWT_SECRET="change-me"
147
- JWT_REFRESH_SECRET="change-me"
170
+ JWT_SECRET=<random — generated for you>
171
+ JWT_REFRESH_SECRET=<random — generated for you>
148
172
  ```
149
173
 
174
+ > **Keep the generated secrets — don't replace them with placeholders like `change-me`.**
175
+ > `SESSION_SECRET` must be **at least 32 characters** or the app throws at
176
+ > startup (`buildCoreSessionOptions` — there is no insecure fallback).
177
+ >
178
+ > **`NODE_ENV`** must be set in the runtime environment. The `start` scripts set
179
+ > it to `development`; set it explicitly in production. Env files resolve as
180
+ > `.env.${NODE_ENV}` first, then `.env` (via `coreEnvFilePath()`), so a plain
181
+ > `.env` works for development and you can add `.env.production` for prod.
182
+
183
+ Baseline HTTP hardening (a whitelisting `ValidationPipe`, CORS, and Helmet) is
184
+ applied for you by `setupCoreSecurity(app, …)` in `src/main.ts`; `helmet` is a
185
+ peer dependency the scaffold already includes.
186
+
150
187
  ### 4) Define Data Models
151
188
 
152
189
  Edit:
@@ -288,6 +325,32 @@ deleteMany: {
288
325
  },
289
326
  ```
290
327
 
328
+ ### Reading Related Models
329
+
330
+ When a query pulls in a relation (`include: { author: true }`, or the equivalent `select`), that related model's **own** read permissions are enforced too — a relation is never a way around ABAC. How this plays out depends on the relation's cardinality.
331
+
332
+ **To-one relations (`belongsTo`, 1:1)** are filtered with *inner-join* semantics: the related model's read `conditions` are applied to the **parent** row.
333
+
334
+ - If the related model has **no** read rule for the caller's role, including the relation raises a `403` (`Missing permissions on model X`). Grant a read rule on every model you expose through an `include`.
335
+ - If the related model has a **conditional** read rule and the specific related row does not satisfy it, the **whole parent row is dropped** from the result (`findFirst` returns `null`; `findMany` omits it) — the parent is *not* returned with the relation set to `null`. Put plainly: you cannot see a row whose related record you are not allowed to see.
336
+
337
+ **To-many relations (`hasMany`, many-to-many)** are filtered in place: the parent row is kept and the child list is narrowed to the rows the caller may read (which can be empty).
338
+
339
+ Example — `Comment.author` points at `User`, and a `User` is only self-readable:
340
+
341
+ ```ts
342
+ User: {
343
+ USER: { findMany: { conditions: { id: PermissionPlaceholder.USER_ID } } },
344
+ },
345
+ Comment: {
346
+ USER: { findMany: { conditions: { authorId: PermissionPlaceholder.USER_ID } } },
347
+ },
348
+ ```
349
+
350
+ `comment.findFirst({ where: { id }, include: { author: true } })` returns the comment only when the caller is also its author (a `User` row they are allowed to read); otherwise the comment itself is filtered out. This is intentional and fails closed.
351
+
352
+ > **Why not return the parent with the relation nulled?** Prisma does not allow a `where` filter on a to-one relation inside `include` / `select`, so the condition is applied to the parent instead. Masking the relation while keeping the parent would require an extra query per included relation; the framework favors the single-query, fail-closed behavior.
353
+
291
354
  ### Action Aliases and Fallbacks
292
355
 
293
356
  To avoid duplicated config, the permissions engine supports fallback behavior. In practice:
package/RELEASING.md ADDED
@@ -0,0 +1,168 @@
1
+ # Releasing
2
+
3
+ This repo publishes **two independent packages**, each versioned on its own:
4
+
5
+ - **`@appxdigital/appx-core`** — the runtime library (root `package.json`),
6
+ released via **`v*`** tags (`release.yml`).
7
+ - **`@appxdigital/appx-core-cli`** — the `appx-core` CLI (`cli/package.json`),
8
+ released via **`cli-v*`** tags (`release-cli.yml`).
9
+
10
+ Both follow the same posture: publishing is deliberate — **it only happens when
11
+ you push a git tag** — and production (`latest`) pauses for a human approval
12
+ before it goes live.
13
+
14
+ ## Channels
15
+
16
+ Library (`v*` tags):
17
+
18
+ | You push tag | npm dist-tag | Consumers install with | Gated? |
19
+ | ------------------ | ------------ | --------------------------------- | --------------- |
20
+ | `v1.2.3-alpha.N` | `alpha` | `…/appx-core@alpha` | no |
21
+ | `v1.2.3-beta.N` | `beta` | `…/appx-core@beta` | no |
22
+ | `v1.2.3-rc.N` | `next` | `…/appx-core@next` | no |
23
+ | `v1.2.3` | `latest` | `…/appx-core` (default) | **yes** — approval |
24
+
25
+ CLI (`cli-v*` tags) — identical channels for `@appxdigital/appx-core-cli`:
26
+
27
+ | You push tag | npm dist-tag | Consumers install with | Gated? |
28
+ | -------------------- | ------------ | ------------------------------- | --------------- |
29
+ | `cli-v1.2.3-beta.N` | `beta` | `…/appx-core-cli@beta` | no |
30
+ | `cli-v1.2.3` | `latest` | `…/appx-core-cli` (default) | **yes** — approval |
31
+
32
+ Prereleases are excluded from `^1.2.x` range resolution, so a project on a caret
33
+ range never pulls one by accident — testers opt in via the tag or an exact
34
+ version. Merging to `main` **never** publishes; it only runs CI (library build +
35
+ tests on mysql and postgres, plus a CLI smoke check).
36
+
37
+ ## Two files, two audiences
38
+
39
+ - **`PRERELEASES.md`** — granular, one entry per alpha/beta build. What a tester
40
+ reads.
41
+ - **`CHANGELOG.md`** — collapsed, one section per production release with
42
+ aggregated changes and consolidated migration steps. What an upgrading project
43
+ reads. When you promote a version, distil its prerelease entries into a single
44
+ `CHANGELOG.md` section.
45
+
46
+ ## One-time setup (before the first publish)
47
+
48
+ 1. **npm Trusted Publisher (OIDC) — one per package.** On npmjs.com → the
49
+ package → *Settings* → *Trusted Publisher* → GitHub Actions. Add **two**,
50
+ both owner `appxdigital`, repo `appx-core`:
51
+ - `@appxdigital/appx-core` → workflow `release.yml`
52
+ - `@appxdigital/appx-core-cli` → workflow `release-cli.yml`
53
+
54
+ This lets CI mint a short-lived publish token per run — **no `NPM_TOKEN`
55
+ secret is stored anywhere**, and every publish carries supply-chain
56
+ provenance.
57
+
58
+ 2. **GitHub `production` environment (the approval gate).** Repo → *Settings* →
59
+ *Environments* → *New environment* → name it exactly `production` → enable
60
+ *Required reviewers* and add yourself (and/or the team). The
61
+ `publish-production` job targets this environment, so a clean tag will wait
62
+ for a click before publishing to `latest`.
63
+
64
+ ## Cutting a release
65
+
66
+ The tag must match `package.json`'s version exactly, or the release job fails
67
+ fast (a guard against mistagging). Convenience scripts keep them in lockstep and
68
+ create the tag for you — they **do not push**, so pushing stays a deliberate
69
+ step:
70
+
71
+ There are two per package — a **test build** and a **production release**:
72
+
73
+ | Script | What it does | Tag | Goes to |
74
+ | ------------------------------- | ------------------------------------- | --------------- | ----------------- |
75
+ | `npm run release:beta` | library test build (`…-beta.N`) | `v…-beta.N` | `beta` channel |
76
+ | `npm run release:production` | promote library to everyone | `vX.Y.Z` | `latest` (gated) |
77
+ | `npm run release:cli:beta` | CLI test build (`…-beta.N`) | `cli-v…-beta.N` | `beta` channel |
78
+ | `npm run release:cli:production`| promote CLI to everyone | `cli-vX.Y.Z` | `latest` (gated) |
79
+
80
+ `release:beta` publishes only to people who opt in with `@beta`; `release:production`
81
+ is what everyone gets by default, and it pauses for your approval before going
82
+ live. Each command bumps the version, commits, and creates the tag, then prints
83
+ the exact `git push origin HEAD --follow-tags` for you to run when ready — they
84
+ never push on their own.
85
+
86
+ Rarer cases go through the underlying script directly:
87
+ `node scripts/release.mjs lib alpha` (alpha channel), `… lib minor` (a feature
88
+ bump), or `… lib X.Y.Z-beta.0` (an explicit version — e.g. the first beta of a
89
+ version already sitting in `package.json`).
90
+
91
+ The manual steps below show what those scripts do under the hood.
92
+
93
+ ### Beta (test in projects first)
94
+
95
+ ```bash
96
+ # set the version + create the matching tag + commit, without auto-pushing
97
+ npm version 0.1.121-beta.0 --message "release: %s"
98
+ git push origin main --follow-tags
99
+ ```
100
+
101
+ The `Release` workflow builds, tests, and publishes to the `beta` dist-tag.
102
+ Then, in a project you want to test against:
103
+
104
+ ```bash
105
+ npm install @appxdigital/appx-core@beta
106
+ ```
107
+
108
+ Iterate with `npm version prerelease --preid beta` (→ `-beta.1`, `-beta.2`, …),
109
+ each push publishing a new granular `PRERELEASES.md` entry.
110
+
111
+ ### Promote to production (`latest`)
112
+
113
+ When the beta is validated in real projects:
114
+
115
+ ```bash
116
+ # drop the prerelease suffix -> clean version
117
+ npm version 0.1.121 --message "release: %s"
118
+ git push origin main --follow-tags
119
+ ```
120
+
121
+ The workflow builds and tests, then **pauses** on the `production` environment.
122
+ Approve it in the repo's *Actions* tab → the running release → *Review
123
+ deployments* → *Approve and deploy*. It then publishes to `latest`.
124
+
125
+ > Alternatively, promotion can be a pure retag without republishing:
126
+ > `npm dist-tag add @appxdigital/appx-core@0.1.121-beta.N latest`. The
127
+ > tag-driven workflow above is preferred because it re-runs tests and records
128
+ > provenance for the exact `latest` artifact.
129
+
130
+ ### Alpha (rough, one or two projects)
131
+
132
+ ```bash
133
+ npm version 0.1.122-alpha.0 --message "release: %s"
134
+ git push origin main --follow-tags
135
+ # in the project: npm install @appxdigital/appx-core@alpha
136
+ ```
137
+
138
+ ### CLI (`@appxdigital/appx-core-cli`)
139
+
140
+ Same flow, but bump `cli/package.json` and use the `cli-v` tag prefix. The tag
141
+ must match `cli/package.json`'s version.
142
+
143
+ ```bash
144
+ # beta:
145
+ npm --prefix cli version 1.0.21-beta.0 --message "release cli: %s"
146
+ git commit -am "release cli: 1.0.21-beta.0" # npm version --prefix does not commit
147
+ git tag cli-v1.0.21-beta.0
148
+ git push origin main --follow-tags
149
+ # test: npx @appxdigital/appx-core-cli@beta create …
150
+
151
+ # production (gated — approve in the Actions tab):
152
+ npm --prefix cli version 1.0.21 --message "release cli: %s"
153
+ git commit -am "release cli: 1.0.21"
154
+ git tag cli-v1.0.21
155
+ git push origin main --follow-tags
156
+ ```
157
+
158
+ > `npm version --prefix cli` bumps `cli/package.json` but does not create the
159
+ > git commit/tag (that only happens when run in the repo root). Commit and tag
160
+ > explicitly with the `cli-v` prefix as shown.
161
+
162
+ ## Manual fallback
163
+
164
+ If CI is unavailable, you can still publish by hand: `rimraf dist && npm run
165
+ build && npm publish --tag <dist-tag> --access public` for the library, and
166
+ `cd cli && npm publish --tag <dist-tag> --access public` for the CLI. This
167
+ bypasses the tests, the approval gate, and provenance, so prefer the tag-driven
168
+ flow.
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.service.d.ts","sourceRoot":"","sources":["../../src/prisma/prisma.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prisma.service.d.ts","sourceRoot":"","sources":["../../src/prisma/prisma.service.ts"],"names":[],"mappings":"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"}
@@ -226,6 +226,37 @@ let PrismaService = exports.PrismaService = PrismaService_1 = class PrismaServic
226
226
  omitFields.forEach((field) => {
227
227
  delete args.select[field];
228
228
  });
229
+ // Omission must also apply to nested relation selections. Without this,
230
+ // a caller reads an omitted field on a RELATED model by nesting the
231
+ // request (e.g. `select: { rel: { select: { secret: true } } }`) — the
232
+ // top-level delete above only covers this model's own fields.
233
+ for (const key of Object.keys(args.select)) {
234
+ const relation = this.getRelation(modelName, key);
235
+ if (!relation) {
236
+ continue; // scalar field (`true`) or `_count` — nothing to recurse
237
+ }
238
+ const relatedOmit = this.getFieldsToOmit(relation.model, userRole);
239
+ const value = args.select[key];
240
+ if (value === true) {
241
+ // `rel: true` returns every scalar of the related model — narrow
242
+ // it to the readable ones.
243
+ args.select[key] = {
244
+ select: this.buildSelectFields(relation.model, relatedOmit, null, null, userRole),
245
+ };
246
+ }
247
+ else if (value && typeof value === 'object') {
248
+ if (value.select || value.include) {
249
+ // Recurse — strips the related model's omitted fields (and
250
+ // deeper) while preserving any where/orderBy/take on `value`.
251
+ args.select[key] = this.applyFieldOmission(relation.model, userRole, value);
252
+ }
253
+ else {
254
+ // Relation args with no explicit projection (e.g. `{ where }`)
255
+ // — inject a select that excludes omitted fields.
256
+ value.select = this.buildSelectFields(relation.model, relatedOmit, null, null, userRole);
257
+ }
258
+ }
259
+ }
229
260
  }
230
261
  else if (args.include) {
231
262
  this.debug(`Found included model '${modelName}', generating select fields`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appxdigital/appx-core",
3
- "version": "0.1.121-beta.4",
3
+ "version": "0.1.121-beta.6",
4
4
  "description": "Appx Core is a library that provides a set of tools to help you build your application faster.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -99,7 +99,10 @@
99
99
  "rxjs": "~7.8.1"
100
100
  },
101
101
  "files": [
102
- "dist"
102
+ "dist",
103
+ "CHANGELOG.md",
104
+ "PRERELEASES.md",
105
+ "RELEASING.md"
103
106
  ],
104
107
  "engines": {
105
108
  "npm": ">=10.0.0",