@appxdigital/appx-core 0.1.125-beta.0 → 0.1.126-beta.0
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 +21 -0
- package/dist/config/generate-controllers.js +1 -1
- package/dist/config/utils.d.ts +16 -0
- package/dist/config/utils.d.ts.map +1 -1
- package/dist/config/utils.js +21 -1
- package/dist/prisma/permissions-validator.d.ts.map +1 -1
- package/dist/prisma/permissions-validator.js +13 -1
- package/docs/permissions.md +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,27 @@ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [0.1.126] — 2026-07-31
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **A freshly scaffolded project now boots.** `cli/scaffold/src/app.module.ts` never imported the `PrismaModule` it ships, and **no framework module provides `PrismaService`** — so `AuthModule` (and the admin module) had an unresolvable dependency and a brand-new project failed at startup with `UnknownDependenciesException`. It only appeared to work once `generate models` had been run, because generated modules import that `@Global()` module as a side effect. The scaffold now imports it explicitly. **Scaffold-only** — existing projects already register it (directly or via a generated module) and need no change.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Generated controller routes are kebab-case.** `@Controller()` prefixes were `model.toLowerCase() + 's'`, which ran words together and doubled a trailing `s` (`AlbumMember` → `albummembers`, `PublishSettings` → `publishsettingss`). They now match the file names the same command generates: `album-members`, `publish-settings`, `type-samples`. No English pluralization rules are applied (model names are not necessarily English) — the kebab-cased name gets an `s` unless it already ends in one.
|
|
26
|
+
|
|
27
|
+
> **Only affects newly generated controllers.** Generated controllers are hand-owned (written once, never overwritten), so existing routes are untouched and no deployed API changes. A project that generates *new* modules after upgrading will have both styles side by side; rename the older `@Controller()` prefixes by hand if you want consistency.
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
|
|
31
|
+
- **`permissions.md`** gains a "migrating from row-level security" callout — transcribing an RLS `INSERT` policy that checks a joined table (`create: {conditions: {album: …}}`) is a boot error here, because create conditions judge the created model's own scalar fields only and everything crossing a relation belongs to `connect`.
|
|
32
|
+
- **`setUserIdField` and `connect` documented together.** A server-set owner FK still needs a `connect` rule; the docs now show the relation-scoped self-connect (`relations.owner.connect: {conditions: {id: '$USER_ID'}}`) and explain why that is preferable to a blanket `connect: 'ALL'` on the target. The boot validator emits this same targeted hint when the offending column is the one named by `setUserIdField`.
|
|
33
|
+
|
|
34
|
+
Drop-in: no changes required in existing projects.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
17
38
|
## [0.1.125] — 2026-07-29
|
|
18
39
|
|
|
19
40
|
A string (uuid/cuid) `User.id` is now supported end-to-end, and the CLI's `create` gains a non-interactive mode plus far better failure diagnostics.
|
|
@@ -40,7 +40,7 @@ import { CoreController, Permission } from '@appxdigital/appx-core';
|
|
|
40
40
|
import { Create${model}Dto } from './dto/create-${folder}.dto';
|
|
41
41
|
import { Update${model}Dto } from './dto/update-${folder}.dto';
|
|
42
42
|
|
|
43
|
-
@Controller('${
|
|
43
|
+
@Controller('${(0, utils_1.modelRoutePath)(model)}')
|
|
44
44
|
export class ${model}Controller extends CoreController<${model}> {
|
|
45
45
|
constructor(protected readonly service: ${model}Service) {
|
|
46
46
|
super(service);
|
package/dist/config/utils.d.ts
CHANGED
|
@@ -15,6 +15,22 @@
|
|
|
15
15
|
export declare const runProjectBin: (bin: string, args: string, cwd?: string) => void;
|
|
16
16
|
export declare const kebabToPascalCase: (str: string) => string;
|
|
17
17
|
export declare const pascalToKebabCase: (str: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* The HTTP route prefix for a model's generated controller: the kebab-case model
|
|
20
|
+
* name plus a plural 's'.
|
|
21
|
+
*
|
|
22
|
+
* AlbumMember → album-members
|
|
23
|
+
* PublishSettings → publish-settings (already ends in 's' — not doubled)
|
|
24
|
+
* Tenant → tenants
|
|
25
|
+
*
|
|
26
|
+
* Kebab-case so the route matches the file names this generator emits
|
|
27
|
+
* (`album-member.controller.ts`) instead of running the words together.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately NOT a pluralization engine — no English -ies/-es rules, since
|
|
30
|
+
* model names here are not necessarily English. The only guard is against
|
|
31
|
+
* emitting a doubled 's'.
|
|
32
|
+
*/
|
|
33
|
+
export declare const modelRoutePath: (name: string) => string;
|
|
18
34
|
export declare const createFileIfNotExists: (filePath: string, content: string) => void;
|
|
19
35
|
/**
|
|
20
36
|
* Banner stamped on every fully-generated file. These files live under
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,QAAO,MAAM,KAAmB,IAatF,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAGoD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAc,MAAM,WAAW,MAAM,SAInE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UAA0C,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,WAAW,cAAe,MAAM,KAAG,MAAsC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAO,GAAG,EAInC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,GAAG,EAAuE,CAAC;AAEzG,iFAAiF;AACjF,eAAO,MAAM,YAAY,cAAe,MAAM,KAAG,OAGhD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAAc,MAAM,EAAE,KAAG,IAAI,MAAM,CAQnE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAAO,IAAI,MAAM,CAYxC,CAAC"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,QAAO,MAAM,KAAmB,IAatF,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,SAAU,MAAM,KAAG,MAG7C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAGoD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAc,MAAM,WAAW,MAAM,SAInE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UAA0C,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,WAAW,cAAe,MAAM,KAAG,MAAsC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAO,GAAG,EAInC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,GAAG,EAAuE,CAAC;AAEzG,iFAAiF;AACjF,eAAO,MAAM,YAAY,cAAe,MAAM,KAAG,OAGhD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAAc,MAAM,EAAE,KAAG,IAAI,MAAM,CAQnE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAAO,IAAI,MAAM,CAYxC,CAAC"}
|
package/dist/config/utils.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.ownedModels = exports.modelsServedBySource = exports.moduleExists = exports.loadModels = exports.loadAllModels = exports.modelFolder = exports.FRAMEWORK_MODELS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.pascalToKebabCase = exports.kebabToPascalCase = exports.runProjectBin = void 0;
|
|
26
|
+
exports.ownedModels = exports.modelsServedBySource = exports.moduleExists = exports.loadModels = exports.loadAllModels = exports.modelFolder = exports.FRAMEWORK_MODELS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.modelRoutePath = exports.pascalToKebabCase = exports.kebabToPascalCase = exports.runProjectBin = void 0;
|
|
27
27
|
const fs = __importStar(require("fs"));
|
|
28
28
|
const path = __importStar(require("path"));
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
@@ -88,6 +88,26 @@ const pascalToKebabCase = (str) => {
|
|
|
88
88
|
.toLowerCase();
|
|
89
89
|
};
|
|
90
90
|
exports.pascalToKebabCase = pascalToKebabCase;
|
|
91
|
+
/**
|
|
92
|
+
* The HTTP route prefix for a model's generated controller: the kebab-case model
|
|
93
|
+
* name plus a plural 's'.
|
|
94
|
+
*
|
|
95
|
+
* AlbumMember → album-members
|
|
96
|
+
* PublishSettings → publish-settings (already ends in 's' — not doubled)
|
|
97
|
+
* Tenant → tenants
|
|
98
|
+
*
|
|
99
|
+
* Kebab-case so the route matches the file names this generator emits
|
|
100
|
+
* (`album-member.controller.ts`) instead of running the words together.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately NOT a pluralization engine — no English -ies/-es rules, since
|
|
103
|
+
* model names here are not necessarily English. The only guard is against
|
|
104
|
+
* emitting a doubled 's'.
|
|
105
|
+
*/
|
|
106
|
+
const modelRoutePath = (name) => {
|
|
107
|
+
const kebab = (0, exports.pascalToKebabCase)(name);
|
|
108
|
+
return kebab.endsWith('s') ? kebab : `${kebab}s`;
|
|
109
|
+
};
|
|
110
|
+
exports.modelRoutePath = modelRoutePath;
|
|
91
111
|
const createFileIfNotExists = (filePath, content) => {
|
|
92
112
|
console.log(`Creating ${filePath}...`);
|
|
93
113
|
if (!fs.existsSync(filePath)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-validator.d.ts","sourceRoot":"","sources":["../../src/prisma/permissions-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAErF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;AAEnD,MAAM,WAAW,YAAY;IACzB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,wGAAwG;IACxG,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GACf,8BAA8B,GAC9B,0BAA0B,GAC1B,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,EAAE,eAAe,GAC3B,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"permissions-validator.d.ts","sourceRoot":"","sources":["../../src/prisma/permissions-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAErF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;AAEnD,MAAM,WAAW,YAAY;IACzB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,wGAAwG;IACxG,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GACf,8BAA8B,GAC9B,0BAA0B,GAC1B,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,EAAE,eAAe,GAC3B,eAAe,EAAE,CA+HnB"}
|
|
@@ -99,7 +99,19 @@ function validatePermissionsConfig(config, relations) {
|
|
|
99
99
|
}
|
|
100
100
|
if (relRule || hasConnectRule(r.model, role))
|
|
101
101
|
continue;
|
|
102
|
-
|
|
102
|
+
// A server-set owner FK (`setUserIdField`) still needs a connect
|
|
103
|
+
// rule — the value is trusted, but the framework authorizes every
|
|
104
|
+
// relationship a write establishes, uniformly. Point at the
|
|
105
|
+
// relation-scoped self-connect rather than a blanket rule on the
|
|
106
|
+
// target: `connect: 'ALL'` on User would let this role attach ANY
|
|
107
|
+
// user, which is far wider than what a self-owned create needs.
|
|
108
|
+
const createRule = rules?.create;
|
|
109
|
+
const ownerField = typeof createRule === 'object' && createRule
|
|
110
|
+
? createRule.setUserIdField
|
|
111
|
+
: undefined;
|
|
112
|
+
const fix = ownerField === col
|
|
113
|
+
? `'${col}' is server-set via setUserIdField, but the relationship it creates still needs a connect rule. Scope it to the caller with '${modelKey}.${role}.relations.${r.field}.connect: {conditions: {id: "$USER_ID"}}' — prefer that over a blanket '${r.model}.${role}.connect', which would allow connecting to any ${r.model}.`
|
|
114
|
+
: `Declare '${r.model}.${role}.connect' (or 'ALL'), or scope it to this relation with '${modelKey}.${role}.relations.${r.field}.connect'.`;
|
|
103
115
|
if (settableOnCreate && r.isRequired) {
|
|
104
116
|
issues.push({
|
|
105
117
|
level: 'error',
|
package/docs/permissions.md
CHANGED
|
@@ -59,6 +59,8 @@ Shape: `PermissionsConfig[Model][Role][action]`, where each `action` value is ei
|
|
|
59
59
|
|
|
60
60
|
> `aggregate` is **not** a supported permission action yet — see [limitations.md](./limitations.md).
|
|
61
61
|
|
|
62
|
+
> **Migrating from row-level security (Supabase/Postgres RLS)?** Read this section before transcribing your policies. An RLS `INSERT` policy routinely checks a *joined* table (`album.owner = auth.uid()`), so the natural first attempt is `create: {conditions: {album: canEditAlbum}}` — **that is a boot error here.** A `create` condition may only reference the created model's **own scalar fields**; everything that crosses a relation belongs in the [`connect`](#the-connect-action--authorizes-every-relationship-a-write-establishes) action instead. The split is deliberate: "may this row exist with these values" and "may this caller attach it to *that* row" are separate questions, and the second one is checked against the target's rule.
|
|
63
|
+
|
|
62
64
|
### The `connect` action — authorizes every relationship a write establishes
|
|
63
65
|
|
|
64
66
|
A `create` condition judges the created model's **own scalar fields only** — it never reaches across a relation. All relationship authorization lives in the `connect` action. Whenever a **write** links a row to model `T` — a `create` that sets the association, **or an `update` that re-points a foreign key** — the framework checks **`T`'s `connect` rule**, a **dedicated** action separate from reads (*being able to see a record is not permission to associate it* — seeing a team ≠ being allowed to join it). This applies however the link is supplied:
|
|
@@ -128,7 +130,18 @@ So a minimal USER config often needs just `findMany`, `create`, `update`, `delet
|
|
|
128
130
|
|
|
129
131
|
- **`/// @Role(ADMIN)` on a schema column** — only listed roles can *read* that field. For everyone else the proxy omits it from results (it comes back `undefined`). `/// @Role(none)` hides it from all roles. Applies at every depth, including nested relation selections.
|
|
130
132
|
- **`/// @NoWrite` on a schema column** — excludes the field from generated create/update DTOs (all roles). Use it for privileged columns (e.g. `role`) that should be set out-of-band, not through generic CRUD.
|
|
131
|
-
- **`setUserIdField: 'ownerId'`** — the framework sets that field to the caller's id server-side on create (so a client can't forge ownership).
|
|
133
|
+
- **`setUserIdField: 'ownerId'`** — the framework sets that field to the caller's id server-side on create (so a client can't forge ownership). **It does not exempt the field from the `connect` requirement**: `ownerId` is still a foreign key, and every relationship a write establishes is authorized. Declare a **relation-scoped** self-connect rather than a blanket rule on the target:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
Album: {
|
|
137
|
+
USER: {
|
|
138
|
+
create: {setUserIdField: 'ownerId'},
|
|
139
|
+
relations: {owner: {connect: {conditions: {id: '$USER_ID'}}}},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Prefer that over `User.USER.connect: 'ALL'`, which would let the role attach *any* user — much wider than a self-owned create needs.
|
|
132
145
|
|
|
133
146
|
> For the generated request-body DTOs — their structure, and the ways to **remove a writable field** (`OmitType`, `/// @NoWrite`), including how to handle per-role differences — see **[dtos.md](./dtos.md)**. (The old per-role `restrictedFields` strip has been removed — see the migration note there.)
|
|
134
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appxdigital/appx-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.126-beta.0",
|
|
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",
|