@appxdigital/appx-core 0.1.128-beta.3 → 0.1.128-beta.4
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 +2 -0
- package/dist/common/decorators/field-requires.decorator.d.ts +50 -0
- package/dist/common/decorators/field-requires.decorator.d.ts.map +1 -0
- package/dist/common/decorators/field-requires.decorator.js +54 -0
- package/dist/graphql/generic.resolver.d.ts.map +1 -1
- package/dist/graphql/generic.resolver.js +83 -2
- package/dist/graphql/graphql.module.d.ts.map +1 -1
- package/dist/graphql/graphql.module.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/prisma/prisma.service.d.ts +15 -0
- package/dist/prisma/prisma.service.d.ts.map +1 -1
- package/dist/prisma/prisma.service.js +17 -0
- package/docs/graphql.md +62 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -29,6 +29,8 @@ Read-only GraphQL, reworked into a **namespaced, opt-in-per-module** API.
|
|
|
29
29
|
- **Nested selection is to-one only.** Generated GraphQL model types expose scalars, **to-one** relations, and `_count`; **to-many relations are not nested-selectable** (nested lists have no pagination, so they could return unbounded rows). Fetch a list at the top level with pagination — `track { find(where: { albumId … }, take, skip) }`. Filtering by a to-many relation in `where` (`some`/`every`/`none`) and `_count { … }` still work.
|
|
30
30
|
- **Self-documenting operations.** `find` / `get` / `count` and their `where` arg carry GraphQL descriptions (visible in the Apollo Sandbox); model-field descriptions come from Prisma `///` doc comments as before. See [docs/graphql.md](./docs/graphql.md) for the full `where`/`orderBy`/pagination reference.
|
|
31
31
|
- **Malformed filter values return `400`, not `500`.** A `where`/`orderBy` value Prisma can't parse (e.g. `createdAt: { gt: 2024 }` — a number where a date is expected) now surfaces a clean `400`, instead of an internal error that leaked the raw Prisma invocation.
|
|
32
|
+
- **Output shaping via native field resolvers.** Transform a field on the way out (sign a stored key into a URL, mask, format) or add a computed field with a `@Resolver(() => Model)` + `@ResolveField`, registered in the module. A field resolver reads its source columns from `@Parent()`; ABAC field omission still runs at query time, so a transform never sees a `@Role`-restricted column. New **`@FieldRequires('col')`** (one column, several, or an array) declares which columns a computed field reads so the generated `find`/`get` fetches exactly those; undeclared computed fields fall back to fetching all scalar columns, and relations always stay selection-driven. See [docs/graphql.md](./docs/graphql.md) → *Shaping read output*.
|
|
33
|
+
- **GraphQL introspection on in development.** The Apollo Sandbox (`GET /graphql`) needs introspection, which Apollo Server v4 disables under `NODE_ENV=production` (`INTROSPECTION_DISABLED`). It is now enabled in development and disabled in production by default (defense-in-depth — introspection exposes schema shape, not data, which ABAC still governs). Opt in for a production deployment with `APPX_GRAPHQL_INTROSPECTION=true`.
|
|
32
34
|
|
|
33
35
|
### Fixed
|
|
34
36
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GraphQL schema-field `extensions` key under which {@link FieldRequires}
|
|
3
|
+
* records the source columns. The generated read resolver reads it back from the
|
|
4
|
+
* schema when deciding which columns to fetch
|
|
5
|
+
* (see `generic.resolver.ts` → `selectReadColumns`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const FIELD_REQUIRES_EXTENSION = "requires";
|
|
8
|
+
/**
|
|
9
|
+
* Declare the model columns a custom GraphQL `@ResolveField` reads from its
|
|
10
|
+
* `@Parent()`, so the generated `find` / `get` fetches those columns even when
|
|
11
|
+
* the client didn't select them.
|
|
12
|
+
*
|
|
13
|
+
* Use it on a computed field whose value derives from a column — e.g. a signed
|
|
14
|
+
* `coverUrl` built from a stored `coverKey`. Without it, only the columns the
|
|
15
|
+
* client selected are fetched and `@Parent().coverKey` would be `undefined`.
|
|
16
|
+
*
|
|
17
|
+
* - Declared → only the listed columns are fetched for the field.
|
|
18
|
+
* - Not declared → all of the model's scalar columns are fetched.
|
|
19
|
+
* - Not needed for an in-place transform that reuses an existing field's name.
|
|
20
|
+
*
|
|
21
|
+
* ABAC applies: a column the caller's role may not read is omitted at query time,
|
|
22
|
+
* so the resolver sees it as absent — a transform can never surface a
|
|
23
|
+
* `@Role`-restricted value.
|
|
24
|
+
*
|
|
25
|
+
* Accepts a single column, several columns, or an array.
|
|
26
|
+
*
|
|
27
|
+
* @param columns - The scalar column name(s) the resolver reads from its parent.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* @Resolver(() => Project)
|
|
32
|
+
* export class ProjectFields {
|
|
33
|
+
* constructor(private readonly urls: UrlSigner) {}
|
|
34
|
+
*
|
|
35
|
+
* @ResolveField(() => String, { nullable: true })
|
|
36
|
+
* @FieldRequires('coverKey')
|
|
37
|
+
* coverUrl(@Parent() p: Project) {
|
|
38
|
+
* return p.coverKey ? this.urls.sign(p.coverKey) : null;
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @ResolveField(() => String, { nullable: true })
|
|
42
|
+
* @FieldRequires(['firstName', 'lastName'])
|
|
43
|
+
* fullName(@Parent() p: Project) {
|
|
44
|
+
* return [p.firstName, p.lastName].filter(Boolean).join(' ') || null;
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const FieldRequires: (...columns: (string | string[])[]) => MethodDecorator & ClassDecorator & PropertyDecorator;
|
|
50
|
+
//# sourceMappingURL=field-requires.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-requires.decorator.d.ts","sourceRoot":"","sources":["../../../src/common/decorators/field-requires.decorator.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,aAAa,eAAgB,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,yDACH,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FieldRequires = exports.FIELD_REQUIRES_EXTENSION = void 0;
|
|
4
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
5
|
+
/**
|
|
6
|
+
* The GraphQL schema-field `extensions` key under which {@link FieldRequires}
|
|
7
|
+
* records the source columns. The generated read resolver reads it back from the
|
|
8
|
+
* schema when deciding which columns to fetch
|
|
9
|
+
* (see `generic.resolver.ts` → `selectReadColumns`).
|
|
10
|
+
*/
|
|
11
|
+
exports.FIELD_REQUIRES_EXTENSION = 'requires';
|
|
12
|
+
/**
|
|
13
|
+
* Declare the model columns a custom GraphQL `@ResolveField` reads from its
|
|
14
|
+
* `@Parent()`, so the generated `find` / `get` fetches those columns even when
|
|
15
|
+
* the client didn't select them.
|
|
16
|
+
*
|
|
17
|
+
* Use it on a computed field whose value derives from a column — e.g. a signed
|
|
18
|
+
* `coverUrl` built from a stored `coverKey`. Without it, only the columns the
|
|
19
|
+
* client selected are fetched and `@Parent().coverKey` would be `undefined`.
|
|
20
|
+
*
|
|
21
|
+
* - Declared → only the listed columns are fetched for the field.
|
|
22
|
+
* - Not declared → all of the model's scalar columns are fetched.
|
|
23
|
+
* - Not needed for an in-place transform that reuses an existing field's name.
|
|
24
|
+
*
|
|
25
|
+
* ABAC applies: a column the caller's role may not read is omitted at query time,
|
|
26
|
+
* so the resolver sees it as absent — a transform can never surface a
|
|
27
|
+
* `@Role`-restricted value.
|
|
28
|
+
*
|
|
29
|
+
* Accepts a single column, several columns, or an array.
|
|
30
|
+
*
|
|
31
|
+
* @param columns - The scalar column name(s) the resolver reads from its parent.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* @Resolver(() => Project)
|
|
36
|
+
* export class ProjectFields {
|
|
37
|
+
* constructor(private readonly urls: UrlSigner) {}
|
|
38
|
+
*
|
|
39
|
+
* @ResolveField(() => String, { nullable: true })
|
|
40
|
+
* @FieldRequires('coverKey')
|
|
41
|
+
* coverUrl(@Parent() p: Project) {
|
|
42
|
+
* return p.coverKey ? this.urls.sign(p.coverKey) : null;
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* @ResolveField(() => String, { nullable: true })
|
|
46
|
+
* @FieldRequires(['firstName', 'lastName'])
|
|
47
|
+
* fullName(@Parent() p: Project) {
|
|
48
|
+
* return [p.firstName, p.lastName].filter(Boolean).join(' ') || null;
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
const FieldRequires = (...columns) => (0, graphql_1.Extensions)({ [exports.FIELD_REQUIRES_EXTENSION]: columns.flat() });
|
|
54
|
+
exports.FieldRequires = FieldRequires;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generic.resolver.d.ts","sourceRoot":"","sources":["../../src/graphql/generic.resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"generic.resolver.d.ts","sourceRoot":"","sources":["../../src/graphql/generic.resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AA4GrC;;;;GAIG;AACH,MAAM,WAAW,kBAAkB,CAAC,SAAS,GAAG,GAAG;IAC/C,iFAAiF;IACjF,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,oDAAoD;IACpD,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,8DAA8D;IAC9D,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAuE/F"}
|
|
@@ -16,7 +16,9 @@ exports.CoreGraphqlResolver = void 0;
|
|
|
16
16
|
const graphql_1 = require("@nestjs/graphql");
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const plugins_1 = require("@paljs/plugins");
|
|
19
|
+
const graphql_2 = require("graphql");
|
|
19
20
|
const prisma_service_1 = require("../prisma/prisma.service");
|
|
21
|
+
const field_requires_decorator_1 = require("../common/decorators/field-requires.decorator");
|
|
20
22
|
/**
|
|
21
23
|
* Run a read and translate a Prisma **validation** error into a `400`. A
|
|
22
24
|
* malformed `where`/`orderBy` value (e.g. `createdAt: { gt: 2024 }` — a number
|
|
@@ -36,6 +38,85 @@ async function readOrBadRequest(op) {
|
|
|
36
38
|
throw e;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Turn a `PrismaSelect`-derived selection into the columns actually fetched,
|
|
43
|
+
* resolving the source columns that custom `@ResolveField`s need.
|
|
44
|
+
*
|
|
45
|
+
* `PrismaSelect` narrows the Prisma query to exactly the fields the GraphQL
|
|
46
|
+
* request asked for. That breaks the standard NestJS code-first contract where a
|
|
47
|
+
* `@ResolveField` on the model type receives the full entity as `@Parent()`: a
|
|
48
|
+
* custom field (`coverUrl` derived from a `coverKey` column, or a signed-URL
|
|
49
|
+
* transform of an existing field) can't read a source column the client didn't
|
|
50
|
+
* also select. We restore that contract with a **hybrid** strategy that avoids
|
|
51
|
+
* over-fetching:
|
|
52
|
+
*
|
|
53
|
+
* - A custom field can declare its source columns natively, via the standard
|
|
54
|
+
* NestJS field `extensions`:
|
|
55
|
+
* `@ResolveField(() => String, { extensions: { requires: ['coverKey'] } })`
|
|
56
|
+
* When declared, ONLY those columns are added — no over-fetch.
|
|
57
|
+
* - A custom field that declares nothing falls back to fetching all of the
|
|
58
|
+
* model's scalar columns (safe default: the resolver can read anything from
|
|
59
|
+
* `@Parent()`), at the cost of reading columns the query didn't select.
|
|
60
|
+
* - A query that selects no custom fields fetches exactly what was asked for.
|
|
61
|
+
*
|
|
62
|
+
* Relations stay selection-driven throughout (no unbounded relation fetching).
|
|
63
|
+
* ABAC field omission runs afterward in the proxy and strips `@Role`-restricted
|
|
64
|
+
* columns at select-time, so a caller still only ever receives — and a transform
|
|
65
|
+
* only ever sees — the columns their role may read.
|
|
66
|
+
*
|
|
67
|
+
* Custom field names (no backing column) are dropped from the Prisma `select`:
|
|
68
|
+
* `PrismaSelect` puts every requested GraphQL field there, and passing a name
|
|
69
|
+
* Prisma doesn't recognise throws a validation error. They resolve from the
|
|
70
|
+
* parent afterward, so they must not reach the query.
|
|
71
|
+
*/
|
|
72
|
+
function selectReadColumns(select, info, prisma, modelName) {
|
|
73
|
+
const scalars = new Set(prisma.getScalarFields(modelName));
|
|
74
|
+
const requested = select?.select && typeof select.select === 'object' ? select.select : {};
|
|
75
|
+
// The GraphQL type of the returned rows, to read each field's
|
|
76
|
+
// `extensions.requires` (find → [Model], get → Model; getNamedType unwraps).
|
|
77
|
+
const named = (0, graphql_2.getNamedType)(info.returnType);
|
|
78
|
+
const gqlFields = named instanceof graphql_2.GraphQLObjectType ? named.getFields() : {};
|
|
79
|
+
const out = {};
|
|
80
|
+
const requiredColumns = new Set();
|
|
81
|
+
let needAllScalars = false;
|
|
82
|
+
for (const [key, value] of Object.entries(requested)) {
|
|
83
|
+
if (value && typeof value === 'object') {
|
|
84
|
+
out[key] = value; // a relation (or `_count`) selection — keep as-is
|
|
85
|
+
}
|
|
86
|
+
else if (scalars.has(key)) {
|
|
87
|
+
out[key] = value; // a real scalar column
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// A custom field resolver (no backing column). Prefer its declared
|
|
91
|
+
// dependencies (@FieldRequires); otherwise fall back to all scalars.
|
|
92
|
+
const requires = gqlFields[key]?.extensions?.[field_requires_decorator_1.FIELD_REQUIRES_EXTENSION];
|
|
93
|
+
if (Array.isArray(requires)) {
|
|
94
|
+
for (const col of requires) {
|
|
95
|
+
if (scalars.has(col))
|
|
96
|
+
requiredColumns.add(col);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
needAllScalars = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (needAllScalars) {
|
|
105
|
+
for (const field of scalars)
|
|
106
|
+
out[field] = true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
for (const col of requiredColumns)
|
|
110
|
+
out[col] = true;
|
|
111
|
+
}
|
|
112
|
+
// Guard against an empty select (e.g. only a custom field with `requires: []`)
|
|
113
|
+
// — an empty Prisma `select` is invalid; fall back to all scalars.
|
|
114
|
+
if (Object.keys(out).length === 0) {
|
|
115
|
+
for (const field of scalars)
|
|
116
|
+
out[field] = true;
|
|
117
|
+
}
|
|
118
|
+
return { ...(select && typeof select === 'object' ? select : {}), select: out };
|
|
119
|
+
}
|
|
39
120
|
/**
|
|
40
121
|
* Build a **namespaced, read-only** GraphQL resolver for one model.
|
|
41
122
|
*
|
|
@@ -87,11 +168,11 @@ function CoreGraphqlResolver(bundle) {
|
|
|
87
168
|
return {};
|
|
88
169
|
}
|
|
89
170
|
async find(args, info) {
|
|
90
|
-
const select = new plugins_1.PrismaSelect(info).value;
|
|
171
|
+
const select = selectReadColumns(new plugins_1.PrismaSelect(info).value, info, this.prisma, modelName);
|
|
91
172
|
return readOrBadRequest(() => this.prisma.getModelDelegate(modelName).findMany({ ...args, ...select }));
|
|
92
173
|
}
|
|
93
174
|
async get(args, info) {
|
|
94
|
-
const select = new plugins_1.PrismaSelect(info).value;
|
|
175
|
+
const select = selectReadColumns(new plugins_1.PrismaSelect(info).value, info, this.prisma, modelName);
|
|
95
176
|
return readOrBadRequest(() => this.prisma.getModelDelegate(modelName).findFirst({ ...args, ...select }));
|
|
96
177
|
}
|
|
97
178
|
async count(where) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.module.d.ts","sourceRoot":"","sources":["../../src/graphql/graphql.module.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,qBACa,oBAAoB;IAK7B,QAAQ,IAAI,MAAM;CAGrB;AAED,
|
|
1
|
+
{"version":3,"file":"graphql.module.d.ts","sourceRoot":"","sources":["../../src/graphql/graphql.module.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,qBACa,oBAAoB;IAK7B,QAAQ,IAAI,MAAM;CAGrB;AAED,qBAwBa,aAAa;CAAG"}
|
|
@@ -49,6 +49,15 @@ exports.GraphqlModule = GraphqlModule = __decorate([
|
|
|
49
49
|
// `src/generated/` path at boot (CI, read-only deploys, tests).
|
|
50
50
|
autoSchemaFile: true,
|
|
51
51
|
sortSchema: true,
|
|
52
|
+
// Introspection powers the Apollo Sandbox (GET /graphql). It exposes
|
|
53
|
+
// the schema shape (not data — ABAC still governs every field), so we
|
|
54
|
+
// keep it ON in development for the Sandbox, and OFF in production as
|
|
55
|
+
// defense-in-depth (less recon surface). Opt back in for a production
|
|
56
|
+
// deployment with APPX_GRAPHQL_INTROSPECTION=true. Without this, Apollo
|
|
57
|
+
// Server v4 disables introspection under NODE_ENV=production and the
|
|
58
|
+
// Sandbox fails with `INTROSPECTION_DISABLED`.
|
|
59
|
+
introspection: process.env.NODE_ENV !== 'production' ||
|
|
60
|
+
process.env.APPX_GRAPHQL_INTROSPECTION === 'true',
|
|
52
61
|
}),
|
|
53
62
|
],
|
|
54
63
|
providers: [AppxCoreRootResolver],
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export * from './common/interfaces/storage-service.interface';
|
|
|
37
37
|
export * from './modules/file/file-upload.controller';
|
|
38
38
|
export * from './modules/file/file-upload.service';
|
|
39
39
|
export * from './common/decorators/permission.decorator';
|
|
40
|
+
export * from './common/decorators/field-requires.decorator';
|
|
40
41
|
export * from './common/setup/setup-core-security';
|
|
41
42
|
export * from './common/setup/core-config';
|
|
42
43
|
export * from './common/interceptors/file.interceptor';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAI3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,yBAAyB,CAAC;AACxC,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wCAAwC,CAAC;AACvD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0CAA0C,CAAC;AACzD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAI3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,yBAAyB,CAAC;AACxC,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wCAAwC,CAAC;AACvD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ __exportStar(require("./common/interfaces/storage-service.interface"), exports);
|
|
|
56
56
|
__exportStar(require("./modules/file/file-upload.controller"), exports);
|
|
57
57
|
__exportStar(require("./modules/file/file-upload.service"), exports);
|
|
58
58
|
__exportStar(require("./common/decorators/permission.decorator"), exports);
|
|
59
|
+
__exportStar(require("./common/decorators/field-requires.decorator"), exports);
|
|
59
60
|
__exportStar(require("./common/setup/setup-core-security"), exports);
|
|
60
61
|
__exportStar(require("./common/setup/core-config"), exports);
|
|
61
62
|
__exportStar(require("./common/interceptors/file.interceptor"), exports);
|
|
@@ -229,6 +229,21 @@ export declare class PrismaService {
|
|
|
229
229
|
* @returns The condition with placeholders replaced by actual values.
|
|
230
230
|
*/
|
|
231
231
|
private static replacePlaceholders;
|
|
232
|
+
/**
|
|
233
|
+
* The scalar (and enum) field names of a model, as derived from the Prisma
|
|
234
|
+
* DMMF at bootstrap. Relations are excluded.
|
|
235
|
+
*
|
|
236
|
+
* Used by the GraphQL generic resolver to fetch the model's own columns even
|
|
237
|
+
* when a query only selects a subset, so that a custom `@ResolveField` on the
|
|
238
|
+
* model type (e.g. a signed-URL transform) can read its source column from
|
|
239
|
+
* `@Parent()`. ABAC field omission still runs afterward at select-time, so a
|
|
240
|
+
* `@Role`-restricted column is never actually fetched — callers only ever get
|
|
241
|
+
* the readable projection.
|
|
242
|
+
*
|
|
243
|
+
* @param modelName - The model name (any casing; matched case-insensitively).
|
|
244
|
+
* @returns The model's scalar field names, or `[]` if the model is unknown.
|
|
245
|
+
*/
|
|
246
|
+
getScalarFields(modelName: string): string[];
|
|
232
247
|
/**
|
|
233
248
|
* Retrieves the list of fields to omit based on the user's role.
|
|
234
249
|
* It checks the field configurations stored in `fieldConfigs` and compares them against the role.
|
|
@@ -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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"}
|
|
@@ -981,6 +981,23 @@ let PrismaService = exports.PrismaService = PrismaService_1 = class PrismaServic
|
|
|
981
981
|
}
|
|
982
982
|
return condition;
|
|
983
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* The scalar (and enum) field names of a model, as derived from the Prisma
|
|
986
|
+
* DMMF at bootstrap. Relations are excluded.
|
|
987
|
+
*
|
|
988
|
+
* Used by the GraphQL generic resolver to fetch the model's own columns even
|
|
989
|
+
* when a query only selects a subset, so that a custom `@ResolveField` on the
|
|
990
|
+
* model type (e.g. a signed-URL transform) can read its source column from
|
|
991
|
+
* `@Parent()`. ABAC field omission still runs afterward at select-time, so a
|
|
992
|
+
* `@Role`-restricted column is never actually fetched — callers only ever get
|
|
993
|
+
* the readable projection.
|
|
994
|
+
*
|
|
995
|
+
* @param modelName - The model name (any casing; matched case-insensitively).
|
|
996
|
+
* @returns The model's scalar field names, or `[]` if the model is unknown.
|
|
997
|
+
*/
|
|
998
|
+
getScalarFields(modelName) {
|
|
999
|
+
return this.fieldConfigs[modelName.toLowerCase()]?.scalarFields ?? [];
|
|
1000
|
+
}
|
|
984
1001
|
/**
|
|
985
1002
|
* Retrieves the list of fields to omit based on the user's role.
|
|
986
1003
|
* It checks the field configurations stored in `fieldConfigs` and compares them against the role.
|
package/docs/graphql.md
CHANGED
|
@@ -162,6 +162,68 @@ Always let the generated resolver serve the data — a custom resolver that reac
|
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
165
|
+
## Shaping read output (transforms & computed fields)
|
|
166
|
+
|
|
167
|
+
To transform a field on the way out (sign a stored S3 key into a URL, mask a value, format a date) or to add a computed field (`slug`, `fullName`), attach a field resolver to the generated model type in the model's module:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// src/modules/project/project.fields.ts
|
|
171
|
+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
|
172
|
+
import { FieldRequires } from '@appxdigital/appx-core';
|
|
173
|
+
import { Project } from '../../generated/project/project.model';
|
|
174
|
+
import { UrlSigner } from '../../shared/url-signer.service';
|
|
175
|
+
|
|
176
|
+
@Resolver(() => Project)
|
|
177
|
+
export class ProjectFields {
|
|
178
|
+
constructor(private readonly urls: UrlSigner) {}
|
|
179
|
+
|
|
180
|
+
// transform an existing field in place — same name, same type
|
|
181
|
+
@ResolveField(() => String, { name: 'coverKey', nullable: true })
|
|
182
|
+
coverKey(@Parent() p: Project) {
|
|
183
|
+
return p.coverKey ? this.urls.sign(p.coverKey) : null; // stored key → signed URL
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// add a computed field — declare the column(s) it reads
|
|
187
|
+
@ResolveField(() => String, { nullable: true })
|
|
188
|
+
@FieldRequires('name')
|
|
189
|
+
slug(@Parent() p: Project) {
|
|
190
|
+
return p.name ? p.name.toLowerCase().replace(/\s+/g, '-') : null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
```ts
|
|
195
|
+
// project.module.ts
|
|
196
|
+
providers: [ProjectService, ProjectResolver, ProjectFields]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The resolver runs wherever the model appears, including nested — `project { find { coverKey } }` and `projectMember { find { project { coverKey } } }` both return the transformed value.
|
|
200
|
+
|
|
201
|
+
It reads from the already-filtered parent row, so it only ever sees columns the caller's role may read. A field computed from a `@Role`-restricted column receives that column as absent, so a transform cannot surface a value the role can't read.
|
|
202
|
+
|
|
203
|
+
### `@FieldRequires` — declare a computed field's source columns
|
|
204
|
+
|
|
205
|
+
A computed field reads columns from its parent, but the query fetches only the columns the client selected. Declare the columns the field reads so they are fetched even when the client didn't select them:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
@ResolveField(() => String, { nullable: true })
|
|
209
|
+
@FieldRequires('coverKey') // one column
|
|
210
|
+
coverUrl(@Parent() p: Project) { … }
|
|
211
|
+
|
|
212
|
+
@ResolveField(() => String, { nullable: true })
|
|
213
|
+
@FieldRequires(['firstName', 'lastName']) // a list, or @FieldRequires('firstName', 'lastName')
|
|
214
|
+
fullName(@Parent() p: Project) { … }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- **Declared** → only those columns are fetched for the field.
|
|
218
|
+
- **Not declared** → all of the model's scalar columns are fetched, so the field can read any of them from its parent.
|
|
219
|
+
- **In-place transform** (a resolver reusing an existing field's name) → needs no declaration; that column is fetched whenever the client selects the field.
|
|
220
|
+
|
|
221
|
+
Read from `@Parent()`, not the raw Prisma client — re-querying bypasses ABAC. For key-to-URL transforms, reuse the existing field's name so the raw stored value never leaves the server.
|
|
222
|
+
|
|
223
|
+
> The REST CRUD endpoints return rows as stored; output shaping applies to the GraphQL read API.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
165
227
|
## Not available yet
|
|
166
228
|
|
|
167
229
|
- **Mutations** (create/update/delete) — use REST.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appxdigital/appx-core",
|
|
3
|
-
"version": "0.1.128-beta.
|
|
3
|
+
"version": "0.1.128-beta.4",
|
|
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",
|