@appxdigital/appx-core 0.1.127 → 0.1.128-beta.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 CHANGED
@@ -14,6 +14,37 @@ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
14
14
 
15
15
  ---
16
16
 
17
+ ## [0.1.128] — 2026-08-04
18
+
19
+ Read-only GraphQL, reworked into a **namespaced, opt-in-per-module** API.
20
+
21
+ ### Added
22
+
23
+ - **Namespaced read API.** Each exposed model gets one root query field (its camelCase name) with three operations: `find` (→ list), `get` (→ first match or null), `count` (→ Int). Example: `query { project { find(where:{…}){ id } get(where:{…}){ id } count } }`. Short names live on the per-model namespace, so they never collide, aliases work (`recent: find(...)`), and several models can be queried in one request.
24
+ - **Opt-in per module — one line, no resolver file.** Expose a model by importing its generated resolver and adding it to `providers`: `import { <Model>Resolver } from '../../generated/<model>/graphql'; providers: [<Model>Service, <Model>Resolver]`. The generated module ships this commented, copy-paste-ready. `<Model>Resolver` (and the underlying `<Model>Graphql` bundle) live at `src/generated/<model>/graphql.ts`, emitted by the deploy-safe pass. Remove the line to un-expose.
25
+ - **`/graphql` mounted by default** in new projects (Apollo; Sandbox on `GET /graphql`). The schema stays valid before any model opts in.
26
+ - **Same ABAC as REST, verified end-to-end** (`test/http/graphql-abac.spec.ts`): unauthorized rows aren't returned (`count` included — it's filtered to accessible rows), restricted fields (`@Role`) come back null at the top level and nested, and an inaccessible nested **record** is filtered while the parent stays accessible. GraphQL requests now populate the user across transports (`UserPopulationGuard` uses `transformContext`).
27
+ - **Filter/sort limited to readable fields.** A client `where` / `orderBy` / `distinct` that references a field the caller's role can't read is **rejected** (`403`), recursing through relations and `AND`/`OR`/`NOT` — the input-side counterpart to output field omission, applied in `PrismaService` for every transport.
28
+
29
+ ### Changed / Removed
30
+
31
+ - The old root-level `findAll<Model>s` / `findOne<Model>` / `findFirst<Model>` queries and the `GenericResolverFactory` export are **removed**, replaced by the generated per-model `<Model>Resolver` (built on the exported `CoreGraphqlResolver`).
32
+ - `appx-core generate models` **no longer scaffolds a per-module `*.resolver.ts` file**; GraphQL is opt-in via the one-liner above.
33
+ - **`@apollo/server` is now a declared peer dependency** (`GraphqlModule` mounts Apollo by default). A normal `npm install` pulls it — no manual install.
34
+
35
+ ### Migration
36
+
37
+ > **Behaviour change — the GraphQL resolver API changed and per-module resolver files are no longer generated.** Only relevant if you use GraphQL.
38
+
39
+ 1. **Delete the generated `src/modules/<model>/<model>.resolver.ts` files** — they import the removed `GenericResolverFactory` and will not compile. Remove `<Model>Resolver` from each module's `providers`.
40
+ 2. Run **`appx-core generate`** to emit the new `src/generated/<model>/graphql.ts` bundles, then **`npm install`** (pulls the newly-declared `@apollo/server` peer that `GraphqlModule` needs).
41
+ 3. Add **`GraphqlModule`** to your `app.module` imports (new projects already have it): `import { GraphqlModule } from '@appxdigital/appx-core';`.
42
+ 4. **Expose each model** you want in GraphQL by adding one provider line to its module: import `<Model>Resolver` from `../../generated/<model>/graphql` and add it to `providers`. See [docs/graphql.md](./docs/graphql.md).
43
+ 5. **Update clients:** `findAllProjects(...)` → `project { find(...) }`, `findFirstProject` / `findOneProject` → `project { get(...) }`. Mutations remain REST-only.
44
+ 6. **If a client filtered or ordered by a field a role can't read, that now returns `403`** — move such queries to a role that may read the field, or don't reference it.
45
+
46
+ ---
47
+
17
48
  ## [0.1.127] — 2026-08-03
18
49
 
19
50
  ### Changed
package/README.md CHANGED
@@ -81,7 +81,11 @@ AppX Core puts **security and data-access control first**, while keeping develop
81
81
  - JWT auth (recommended)
82
82
  - **Fast CRUD acceleration**
83
83
  - Generated modules, controllers, and services per model
84
- - Standard REST CRUD endpoints
84
+ - Standard REST CRUD endpoints (the write surface)
85
+ - **GraphQL read API — recommended for querying**
86
+ - Namespaced, **opt-in per model**: `model { find get count }`
87
+ - **Native pagination** (`take` / `skip` / `cursor`), nested relations, and several models per request — prefer it over the CRUD list endpoint for reads/filtering, and enable it on your main queryable models
88
+ - Same ABAC as REST (row / field / nested-record filtering; filter & sort limited to readable fields)
85
89
  - **Developer-friendly extension points**
86
90
  - Add custom endpoints with `@Permission(...)`
87
91
  - Use services for business logic, controllers as transport layer
@@ -614,7 +618,7 @@ Operational guidance:
614
618
 
615
619
  ### GraphQL status
616
620
 
617
- GraphQL is part of the direction, but is **not fully ready** in the current release. REST CRUD + AdminJS are the primary integration surfaces for now.
621
+ GraphQL exposes a **read-only** API — `find` / `get` / `count`, namespaced per model and **opt-in per module** (one line) — that runs through the same ABAC proxy as REST: row filtering, field omission, nested-relation filtering, and filter/sort limited to fields the role can read all apply. It supports nested selections and native pagination (`take`/`skip`/`cursor`), so it's the recommended surface for querying/filtering/paginating reads. See **[docs/graphql.md](./docs/graphql.md)**. Not yet: mutations and full aggregates — REST CRUD + AdminJS remain the write surfaces.
618
622
 
619
623
  ---
620
624
 
@@ -1 +1 @@
1
- {"version":3,"file":"user-population.guard.d.ts","sourceRoot":"","sources":["../../../src/common/guards/user-population.guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAE9D,qBACa,mBAAoB,YAAW,WAAW;IAE/C,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAFb,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa;IAG3C,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAyBjE"}
1
+ {"version":3,"file":"user-population.guard.d.ts","sourceRoot":"","sources":["../../../src/common/guards/user-population.guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAG9D,qBACa,mBAAoB,YAAW,WAAW;IAE/C,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAFb,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa;IAG3C,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CA4BjE"}
@@ -14,6 +14,7 @@ const common_1 = require("@nestjs/common");
14
14
  const jwt_1 = require("@nestjs/jwt");
15
15
  const config_1 = require("@nestjs/config");
16
16
  const auth_service_1 = require("../../modules/auth/auth.service");
17
+ const context_transformer_util_1 = require("../utils/context-transformer.util");
17
18
  let UserPopulationGuard = exports.UserPopulationGuard = class UserPopulationGuard {
18
19
  constructor(jwtService, authService, configService) {
19
20
  this.jwtService = jwtService;
@@ -21,7 +22,10 @@ let UserPopulationGuard = exports.UserPopulationGuard = class UserPopulationGuar
21
22
  this.configService = configService;
22
23
  }
23
24
  async canActivate(context) {
24
- const request = context.switchToHttp().getRequest();
25
+ // Resolve the request across transports (HTTP and GraphQL) so JWT/session
26
+ // population also applies to GraphQL requests — otherwise a GraphQL query
27
+ // would reach the ABAC proxy with no user and be treated as a GUEST.
28
+ const { req: request } = (0, context_transformer_util_1.transformContext)(context);
25
29
  if (request.isAuthenticated && request.isAuthenticated()) {
26
30
  return true;
27
31
  }
@@ -17,9 +17,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  */
18
18
  const utils_1 = require("./utils");
19
19
  const generate_dtos_1 = require("./generate-dtos");
20
+ const generate_graphql_1 = require("./generate-graphql");
20
21
  console.log('Running Prisma Generate (client + GraphQL artifacts)...');
21
22
  // Project-local prisma: the CLI is usually global, so PATH has no project .bin.
22
23
  (0, utils_1.runProjectBin)('prisma', 'generate');
24
+ const allModels = (0, utils_1.loadAllModels)();
23
25
  console.log('Generating DTO base classes...');
24
- (0, generate_dtos_1.generateDtoBases)((0, utils_1.loadAllModels)());
26
+ (0, generate_dtos_1.generateDtoBases)(allModels);
27
+ console.log('Generating GraphQL bundles...');
28
+ (0, generate_graphql_1.generateGraphqlBundles)(allModels);
25
29
  console.log('Deploy-safe generation complete — src/generated only, no code changes.');
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Emit the GraphQL bundle for every model. Part of the deploy-safe pass — it
3
+ * only writes under `src/generated/**`, overwriting each time so the bundle
4
+ * tracks the schema. Requires `prisma generate` (the GraphQL artifacts) to have
5
+ * run first; a model whose generated folder is missing is skipped with a note.
6
+ */
7
+ export declare function generateGraphqlBundles(allModels: {
8
+ name: string;
9
+ }[]): void;
10
+ //# sourceMappingURL=generate-graphql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-graphql.d.ts","sourceRoot":"","sources":["../../src/config/generate-graphql.ts"],"names":[],"mappings":"AA6CA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,EAAE,GAAG,IAAI,CAUxE"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.generateGraphqlBundles = void 0;
27
+ const fs = __importStar(require("fs"));
28
+ const path = __importStar(require("path"));
29
+ const utils_1 = require("./utils");
30
+ const generatedRoot = path.join(process.cwd(), 'src/generated');
31
+ /**
32
+ * The per-model GraphQL bundle: re-exports the `prisma-nestjs-graphql` types
33
+ * `CoreGraphqlResolver` needs (model + find/get/count inputs), as one import.
34
+ *
35
+ * Emitted for every model into `src/generated/<folder>/graphql.ts` (gitignored,
36
+ * overwrite-safe) so opting a module into GraphQL is a single import + a single
37
+ * provider line — no hand-written resolver file:
38
+ *
39
+ * import { <Model>Graphql } from '../../generated/<folder>/graphql';
40
+ * providers: [<Model>Service, CoreGraphqlResolver(<Model>Graphql)]
41
+ */
42
+ const bundleTemplate = (model, folder) => {
43
+ const camel = model.charAt(0).toLowerCase() + model.slice(1);
44
+ return `import { CoreGraphqlResolver, GraphqlModelBundle } from '@appxdigital/appx-core';
45
+ import { ${model} } from './${folder}.model';
46
+ import { FindMany${model}Args } from './find-many-${folder}.args';
47
+ import { FindFirst${model}Args } from './find-first-${folder}.args';
48
+ import { ${model}WhereInput } from './${folder}-where.input';
49
+
50
+ export const ${model}Graphql: GraphqlModelBundle<${model}> = {
51
+ model: ${model},
52
+ name: '${camel}',
53
+ findManyArgs: FindMany${model}Args,
54
+ findFirstArgs: FindFirst${model}Args,
55
+ whereInput: ${model}WhereInput,
56
+ };
57
+
58
+ /**
59
+ * Ready-to-register read-only GraphQL resolver for ${model} (\`${camel} { find get count }\`).
60
+ * Add it to your ${model} module's \`providers\` to expose the model — no CoreGraphqlResolver
61
+ * import or resolver file needed:
62
+ *
63
+ * import { ${model}Resolver } from '../../generated/${folder}/graphql';
64
+ * providers: [${model}Service, ${model}Resolver]
65
+ */
66
+ export const ${model}Resolver = CoreGraphqlResolver(${model}Graphql);
67
+ `;
68
+ };
69
+ /**
70
+ * Emit the GraphQL bundle for every model. Part of the deploy-safe pass — it
71
+ * only writes under `src/generated/**`, overwriting each time so the bundle
72
+ * tracks the schema. Requires `prisma generate` (the GraphQL artifacts) to have
73
+ * run first; a model whose generated folder is missing is skipped with a note.
74
+ */
75
+ function generateGraphqlBundles(allModels) {
76
+ for (const { name } of allModels) {
77
+ const folder = (0, utils_1.modelFolder)(name);
78
+ const dir = path.join(generatedRoot, folder);
79
+ if (!fs.existsSync(dir)) {
80
+ console.warn(`Skipping GraphQL bundle for ${name}: ${dir} not found (run prisma generate first).`);
81
+ continue;
82
+ }
83
+ fs.writeFileSync(path.join(dir, 'graphql.ts'), bundleTemplate(name, folder));
84
+ }
85
+ }
86
+ exports.generateGraphqlBundles = generateGraphqlBundles;
@@ -27,7 +27,6 @@ const generate_dtos_1 = require("./generate-dtos");
27
27
  const generate_modules_1 = require("./generate-modules");
28
28
  const generate_services_1 = require("./generate-services");
29
29
  const generate_controllers_1 = require("./generate-controllers");
30
- const generate_resolvers_1 = require("./generate-resolvers");
31
30
  // inquirer is ESM-only; this file compiles to CommonJS, where tsc would
32
31
  // down-level a normal `import('inquirer')` into `require()` (ERR_REQUIRE_ESM at
33
32
  // runtime). Wrapping in `new Function` keeps a genuine dynamic `import()` that
@@ -95,7 +94,6 @@ async function main() {
95
94
  (0, generate_modules_1.scaffoldModule)(name);
96
95
  (0, generate_services_1.scaffoldService)(name);
97
96
  (0, generate_controllers_1.scaffoldController)(name);
98
- (0, generate_resolvers_1.scaffoldResolver)(name);
99
97
  (0, generate_dtos_1.scaffoldDtoSubclass)(name);
100
98
  }
101
99
  (0, generate_modules_1.registerModulesInAppModule)(selected);
@@ -1 +1 @@
1
- {"version":3,"file":"generate-modules.d.ts","sourceRoot":"","sources":["../../src/config/generate-modules.ts"],"names":[],"mappings":"AA2BA,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAItD;AAyCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,GACrB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAkDpC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CASrE"}
1
+ {"version":3,"file":"generate-modules.d.ts","sourceRoot":"","sources":["../../src/config/generate-modules.ts"],"names":[],"mappings":"AAmCA,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAItD;AAyCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CACvC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,GACrB;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAkDpC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CASrE"}
@@ -34,20 +34,28 @@ const appModulePath = path.join(process.cwd(), 'src/app.module.ts');
34
34
  * @param model
35
35
  * @param folder
36
36
  */
37
- const moduleTemplate = (model, folder) => `
37
+ const moduleTemplate = (model, folder) => {
38
+ const camel = model.charAt(0).toLowerCase() + model.slice(1);
39
+ return `
38
40
  import { Module } from '@nestjs/common';
39
41
  import { PrismaModule } from '../../prisma/prisma.module';
40
42
  import { ${model}Controller } from './${folder}.controller';
41
43
  import { ${model}Service } from './${folder}.service';
42
- import { ${model}Resolver } from './${folder}.resolver';
44
+ // To expose this model over GraphQL (read-only: \`${camel} { find get count }\`),
45
+ // uncomment the import and the provider line below. See docs/graphql.md.
46
+ // import { ${model}Resolver } from '../../generated/${folder}/graphql';
43
47
 
44
48
  @Module({
45
49
  imports: [PrismaModule],
46
50
  controllers: [${model}Controller],
47
- providers: [${model}Service, ${model}Resolver],
51
+ providers: [
52
+ ${model}Service,
53
+ // ${model}Resolver,
54
+ ],
48
55
  })
49
56
  export class ${model}Module {}
50
57
  `;
58
+ };
51
59
  /** Scaffold the module file for a single model (once; never overwritten). */
52
60
  function scaffoldModule(modelName) {
53
61
  const folder = (0, utils_1.modelFolder)(modelName);
@@ -1,15 +1,51 @@
1
1
  import { Type } from '../common/types';
2
- import { GraphQLResolveInfo } from 'graphql';
3
- import { PrismaService } from '../prisma/prisma.service';
4
- export declare function GenericResolverFactory<ModelType, CreateInput, UpdateInput, WhereInput, WhereUniqueInput, FindManyArgsType, AggregateArgsType, AggregateOutputType, CreateManyArgsType, CreateManyInput>(model: string, returnType: Type<ModelType>, createInputType: Type<CreateInput>, updateInputType: Type<UpdateInput>, whereInputType: Type<WhereInput>, whereUniqueInputType: Type<WhereUniqueInput>, findManyArgsType: Type<FindManyArgsType>, aggregateArgsType: Type<AggregateArgsType>, aggregateOutputType: Type<AggregateOutputType>, createManyArgsType: Type<CreateManyArgsType>, createManyInputType: Type<CreateManyInput>, guardedMethods?: {
5
- [key: string]: string[];
6
- }): {
7
- new (prisma: PrismaService): {
8
- readonly prisma: PrismaService;
9
- findAll(args: FindManyArgsType, info: GraphQLResolveInfo): Promise<ModelType[]>;
10
- findOne(where: WhereUniqueInput, info: GraphQLResolveInfo): Promise<ModelType>;
11
- findFirst(args: FindManyArgsType, info: GraphQLResolveInfo): Promise<ModelType>;
12
- aggregate(args: AggregateArgsType, info: GraphQLResolveInfo): Promise<AggregateOutputType>;
13
- };
14
- };
2
+ /**
3
+ * The generated `prisma-nestjs-graphql` types a model needs to expose its
4
+ * read API. Produced per model by the deploy-safe generator as
5
+ * `src/generated/<model>/graphql.ts` (see `generate-graphql.ts`).
6
+ */
7
+ export interface GraphqlModelBundle<ModelType = any> {
8
+ /** The `@ObjectType()` model class (also used to derive the root field name). */
9
+ model: Type<ModelType>;
10
+ /** Root query field name. Defaults to camelCase(model name), e.g. `project`. */
11
+ name?: string;
12
+ /** `FindMany<Model>Args` — arguments for `find`. */
13
+ findManyArgs: Type<any>;
14
+ /** `FindFirst<Model>Args` — arguments for `get`. */
15
+ findFirstArgs: Type<any>;
16
+ /** `<Model>WhereInput` — the `where` argument for `count`. */
17
+ whereInput: Type<any>;
18
+ }
19
+ /**
20
+ * Build a **namespaced, read-only** GraphQL resolver for one model.
21
+ *
22
+ * Every model contributes a single root query field (its camelCase name) that
23
+ * returns a per-model namespace type exposing three operations:
24
+ *
25
+ * query {
26
+ * project { # one root field per model — collision-free
27
+ * find(where, orderBy, take, …) # → [Project] (Prisma findMany)
28
+ * get(where, …) # → Project|null (Prisma findFirst)
29
+ * count(where) # → Int (Prisma count)
30
+ * }
31
+ * }
32
+ *
33
+ * Because the operation names (`find`/`get`/`count`) live on the namespace type
34
+ * rather than the root, they repeat freely across models and aliases work as
35
+ * expected (`recent: find(...)`, plus multiple models in one request).
36
+ *
37
+ * Every operation routes through `PrismaService.getModelDelegate`, so the exact
38
+ * same ABAC applies as for REST: row-level filtering, field omission (`@Role`),
39
+ * and nested-relation filtering. `PrismaSelect` turns the GraphQL selection set
40
+ * (including nested relations/children) into a Prisma `select`, so a single
41
+ * query resolves relations in one authorized round-trip.
42
+ *
43
+ * Returned value is a NestJS provider — add it to a module's `providers` to
44
+ * opt that model into GraphQL:
45
+ *
46
+ * import { CoreGraphqlResolver } from '@appxdigital/appx-core';
47
+ * import { ProjectGraphql } from '../../generated/project/graphql';
48
+ * providers: [ProjectService, CoreGraphqlResolver(ProjectGraphql)]
49
+ */
50
+ export declare function CoreGraphqlResolver<ModelType>(bundle: GraphqlModelBundle<ModelType>): Type<any>;
15
51
  //# sourceMappingURL=generic.resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generic.resolver.d.ts","sourceRoot":"","sources":["../../src/graphql/generic.resolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAErC,OAAO,EAAC,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAE3C,OAAO,EAAC,aAAa,EAAC,MAAM,0BAA0B,CAAC;AAEvD,wBAAgB,sBAAsB,CAAC,SAAS,EAC5C,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EAEf,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,EAC3B,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,EAClC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,EAClC,cAAc,EAAE,IAAI,CAAC,UAAU,CAAC,EAChC,oBAAoB,EAAE,IAAI,CAAC,gBAAgB,CAAC,EAC5C,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,CAAC,EACxC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAC1C,mBAAmB,EAAE,IAAI,CAAC,mBAAmB,CAAC,EAC9C,kBAAkB,EAAE,IAAI,CAAC,kBAAkB,CAAC,EAC5C,mBAAmB,EAAE,IAAI,CAAC,eAAe,CAAC,EAC1C,cAAc,CAAC,EAAE;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAC;iBAIF,aAAa;yBAAb,aAAa;sBAWnC,gBAAgB,QACZ,kBAAkB,GACjC,QAAQ,SAAS,EAAE,CAAC;uBAcR,gBAAgB,QACb,kBAAkB,GACjC,QAAQ,SAAS,CAAC;wBAcP,gBAAgB,QACZ,kBAAkB,GACjC,QAAQ,SAAS,CAAC;wBAcP,iBAAiB,QACb,kBAAkB,GACjC,QAAQ,mBAAmB,CAAC;;EA0ItC"}
1
+ {"version":3,"file":"generic.resolver.d.ts","sourceRoot":"","sources":["../../src/graphql/generic.resolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAKrC;;;;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,CA+C/F"}
@@ -12,97 +12,105 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  return function (target, key) { decorator(target, key, paramIndex); }
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.GenericResolverFactory = void 0;
15
+ exports.CoreGraphqlResolver = void 0;
16
16
  const graphql_1 = require("@nestjs/graphql");
17
17
  const plugins_1 = require("@paljs/plugins");
18
- const guard_decorator_1 = require("../common/decorators/guard.decorator");
19
18
  const prisma_service_1 = require("../prisma/prisma.service");
20
- function GenericResolverFactory(model, returnType, createInputType, updateInputType, whereInputType, whereUniqueInputType, findManyArgsType, aggregateArgsType, aggregateOutputType, createManyArgsType, createManyInputType, guardedMethods) {
21
- var GenericResolverClass_1;
22
- let GenericResolverClass = GenericResolverClass_1 = class GenericResolverClass {
19
+ /**
20
+ * Build a **namespaced, read-only** GraphQL resolver for one model.
21
+ *
22
+ * Every model contributes a single root query field (its camelCase name) that
23
+ * returns a per-model namespace type exposing three operations:
24
+ *
25
+ * query {
26
+ * project { # one root field per model — collision-free
27
+ * find(where, orderBy, take, …) # → [Project] (Prisma findMany)
28
+ * get(where, …) # → Project|null (Prisma findFirst)
29
+ * count(where) # → Int (Prisma count)
30
+ * }
31
+ * }
32
+ *
33
+ * Because the operation names (`find`/`get`/`count`) live on the namespace type
34
+ * rather than the root, they repeat freely across models and aliases work as
35
+ * expected (`recent: find(...)`, plus multiple models in one request).
36
+ *
37
+ * Every operation routes through `PrismaService.getModelDelegate`, so the exact
38
+ * same ABAC applies as for REST: row-level filtering, field omission (`@Role`),
39
+ * and nested-relation filtering. `PrismaSelect` turns the GraphQL selection set
40
+ * (including nested relations/children) into a Prisma `select`, so a single
41
+ * query resolves relations in one authorized round-trip.
42
+ *
43
+ * Returned value is a NestJS provider — add it to a module's `providers` to
44
+ * opt that model into GraphQL:
45
+ *
46
+ * import { CoreGraphqlResolver } from '@appxdigital/appx-core';
47
+ * import { ProjectGraphql } from '../../generated/project/graphql';
48
+ * providers: [ProjectService, CoreGraphqlResolver(ProjectGraphql)]
49
+ */
50
+ function CoreGraphqlResolver(bundle) {
51
+ const { model, findManyArgs, findFirstArgs, whereInput } = bundle;
52
+ const modelName = model.name; // Pascal, e.g. 'Project'
53
+ const rootName = bundle.name ?? modelName.charAt(0).toLowerCase() + modelName.slice(1);
54
+ let ModelQueries = class ModelQueries {
55
+ };
56
+ ModelQueries = __decorate([
57
+ (0, graphql_1.ObjectType)(`${modelName}Queries`)
58
+ ], ModelQueries);
59
+ let ModelQueriesResolver = class ModelQueriesResolver {
23
60
  constructor(prisma) {
24
61
  this.prisma = prisma;
25
- if (guardedMethods) {
26
- (0, guard_decorator_1.applyMethodGuards)(GenericResolverClass_1, guardedMethods);
27
- }
28
62
  }
29
- async findAll(args, info) {
30
- const select = new plugins_1.PrismaSelect(info).value;
31
- const modelDelegate = this.prisma.getModelDelegate(model);
32
- return await modelDelegate.findMany({
33
- ...args,
34
- ...select,
35
- });
63
+ // The root field. Returns an empty object; the operations below are
64
+ // resolved lazily as fields of the namespace, so `project { count }`
65
+ // never runs `find`/`get`.
66
+ namespace() {
67
+ return {};
36
68
  }
37
- async findOne(where, info) {
69
+ async find(args, info) {
38
70
  const select = new plugins_1.PrismaSelect(info).value;
39
- const modelDelegate = this.prisma.getModelDelegate(model);
40
- return modelDelegate.findFirst({
41
- where,
42
- ...select,
43
- });
71
+ return this.prisma.getModelDelegate(modelName).findMany({ ...args, ...select });
44
72
  }
45
- async findFirst(args, info) {
73
+ async get(args, info) {
46
74
  const select = new plugins_1.PrismaSelect(info).value;
47
- const modelDelegate = this.prisma.getModelDelegate(model);
48
- return modelDelegate.findFirst({
49
- ...args,
50
- ...select,
51
- });
75
+ return this.prisma.getModelDelegate(modelName).findFirst({ ...args, ...select });
52
76
  }
53
- async aggregate(args, info) {
54
- const select = new plugins_1.PrismaSelect(info).value;
55
- const modelDelegate = this.prisma.getModelDelegate(model);
56
- return modelDelegate.aggregate({
57
- ...args,
58
- ...select,
59
- });
77
+ async count(where) {
78
+ return this.prisma.getModelDelegate(modelName).count({ where });
60
79
  }
61
80
  };
62
81
  __decorate([
63
- (0, graphql_1.Query)(() => [returnType], {
64
- name: `findAll${model.charAt(0).toUpperCase() + model.slice(1)}s`,
65
- }),
66
- __param(0, (0, graphql_1.Args)({ type: () => findManyArgsType, nullable: true })),
67
- __param(1, (0, graphql_1.Info)()),
82
+ (0, graphql_1.Query)(() => ModelQueries, { name: rootName }),
68
83
  __metadata("design:type", Function),
69
- __metadata("design:paramtypes", [Object, Object]),
70
- __metadata("design:returntype", Promise)
71
- ], GenericResolverClass.prototype, "findAll", null);
84
+ __metadata("design:paramtypes", []),
85
+ __metadata("design:returntype", ModelQueries)
86
+ ], ModelQueriesResolver.prototype, "namespace", null);
72
87
  __decorate([
73
- (0, graphql_1.Query)(() => returnType, {
74
- name: `findOne${model.charAt(0).toUpperCase() + model.slice(1)}`,
75
- }),
76
- __param(0, (0, graphql_1.Args)('where', { type: () => whereUniqueInputType, nullable: false })),
88
+ (0, graphql_1.ResolveField)(() => [model], { name: 'find' }),
89
+ __param(0, (0, graphql_1.Args)({ type: () => findManyArgs, nullable: true })),
77
90
  __param(1, (0, graphql_1.Info)()),
78
91
  __metadata("design:type", Function),
79
92
  __metadata("design:paramtypes", [Object, Object]),
80
93
  __metadata("design:returntype", Promise)
81
- ], GenericResolverClass.prototype, "findOne", null);
94
+ ], ModelQueriesResolver.prototype, "find", null);
82
95
  __decorate([
83
- (0, graphql_1.Query)(() => returnType, {
84
- name: `findFirst${model.charAt(0).toUpperCase() + model.slice(1)}`,
85
- }),
86
- __param(0, (0, graphql_1.Args)({ type: () => findManyArgsType, nullable: true })),
96
+ (0, graphql_1.ResolveField)(() => model, { name: 'get', nullable: true }),
97
+ __param(0, (0, graphql_1.Args)({ type: () => findFirstArgs, nullable: true })),
87
98
  __param(1, (0, graphql_1.Info)()),
88
99
  __metadata("design:type", Function),
89
100
  __metadata("design:paramtypes", [Object, Object]),
90
101
  __metadata("design:returntype", Promise)
91
- ], GenericResolverClass.prototype, "findFirst", null);
102
+ ], ModelQueriesResolver.prototype, "get", null);
92
103
  __decorate([
93
- (0, graphql_1.Query)(() => aggregateOutputType, {
94
- name: `aggregate${model.charAt(0).toUpperCase() + model.slice(1)}`,
95
- }),
96
- __param(0, (0, graphql_1.Args)({ type: () => aggregateArgsType, nullable: true })),
97
- __param(1, (0, graphql_1.Info)()),
104
+ (0, graphql_1.ResolveField)(() => graphql_1.Int, { name: 'count' }),
105
+ __param(0, (0, graphql_1.Args)('where', { type: () => whereInput, nullable: true })),
98
106
  __metadata("design:type", Function),
99
- __metadata("design:paramtypes", [Object, Object]),
107
+ __metadata("design:paramtypes", [Object]),
100
108
  __metadata("design:returntype", Promise)
101
- ], GenericResolverClass.prototype, "aggregate", null);
102
- GenericResolverClass = GenericResolverClass_1 = __decorate([
103
- (0, graphql_1.Resolver)({ isAbstract: true }),
109
+ ], ModelQueriesResolver.prototype, "count", null);
110
+ ModelQueriesResolver = __decorate([
111
+ (0, graphql_1.Resolver)(() => ModelQueries),
104
112
  __metadata("design:paramtypes", [prisma_service_1.PrismaService])
105
- ], GenericResolverClass);
106
- return GenericResolverClass;
113
+ ], ModelQueriesResolver);
114
+ return ModelQueriesResolver;
107
115
  }
108
- exports.GenericResolverFactory = GenericResolverFactory;
116
+ exports.CoreGraphqlResolver = CoreGraphqlResolver;
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Always-present root query. GraphQL requires a non-empty `Query` type, but a
3
+ * project may register `GraphqlModule` before any model has opted in (models
4
+ * opt in per-module via `CoreGraphqlResolver`). This trivial field keeps the
5
+ * schema valid in that state so `/graphql` is live from the start.
6
+ */
7
+ export declare class AppxCoreRootResolver {
8
+ appxCore(): string;
9
+ }
1
10
  export declare class GraphqlModule {
2
11
  }
3
12
  //# sourceMappingURL=graphql.module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.module.d.ts","sourceRoot":"","sources":["../../src/graphql/graphql.module.ts"],"names":[],"mappings":"AAMA,qBAUa,aAAa;CAAG"}
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,qBAca,aAAa;CAAG"}
@@ -5,12 +5,37 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.GraphqlModule = void 0;
12
+ exports.GraphqlModule = exports.AppxCoreRootResolver = void 0;
10
13
  const common_1 = require("@nestjs/common");
11
14
  const graphql_1 = require("@nestjs/graphql");
12
- const path_1 = require("path");
13
15
  const apollo_1 = require("@nestjs/apollo");
16
+ /**
17
+ * Always-present root query. GraphQL requires a non-empty `Query` type, but a
18
+ * project may register `GraphqlModule` before any model has opted in (models
19
+ * opt in per-module via `CoreGraphqlResolver`). This trivial field keeps the
20
+ * schema valid in that state so `/graphql` is live from the start.
21
+ */
22
+ let AppxCoreRootResolver = exports.AppxCoreRootResolver = class AppxCoreRootResolver {
23
+ appxCore() {
24
+ return '@appxdigital/appx-core';
25
+ }
26
+ };
27
+ __decorate([
28
+ (0, graphql_1.Query)(() => String, {
29
+ name: '_appxCore',
30
+ description: 'Liveness field. Ensures a valid GraphQL schema even before any model opts in.',
31
+ }),
32
+ __metadata("design:type", Function),
33
+ __metadata("design:paramtypes", []),
34
+ __metadata("design:returntype", String)
35
+ ], AppxCoreRootResolver.prototype, "appxCore", null);
36
+ exports.AppxCoreRootResolver = AppxCoreRootResolver = __decorate([
37
+ (0, graphql_1.Resolver)()
38
+ ], AppxCoreRootResolver);
14
39
  let GraphqlModule = exports.GraphqlModule = class GraphqlModule {
15
40
  };
16
41
  exports.GraphqlModule = GraphqlModule = __decorate([
@@ -19,9 +44,13 @@ exports.GraphqlModule = GraphqlModule = __decorate([
19
44
  graphql_1.GraphQLModule.forRoot({
20
45
  driver: apollo_1.ApolloDriver,
21
46
  context: ({ req, res }) => ({ req, res }),
22
- autoSchemaFile: (0, path_1.join)(process.cwd(), 'src/generated/schema.gql'),
47
+ // Build the schema in memory (code-first). Nothing consumes the SDL
48
+ // file, and an in-memory schema avoids depending on a writable
49
+ // `src/generated/` path at boot (CI, read-only deploys, tests).
50
+ autoSchemaFile: true,
23
51
  sortSchema: true,
24
52
  }),
25
53
  ],
54
+ providers: [AppxCoreRootResolver],
26
55
  })
27
56
  ], GraphqlModule);
@@ -102,6 +102,18 @@ export declare class PrismaService {
102
102
  * @returns The modified query arguments with the appropriate `where` conditions applied.
103
103
  * @throws ForbiddenException if no valid `where` conditions are present after applying permissions.
104
104
  */
105
+ /**
106
+ * Reject a client `where` / `orderBy` / `distinct` that references a field
107
+ * this role cannot read (per `@Role` / `getFieldsToOmit`) — the input-side
108
+ * counterpart to `applyFieldOmission` on output, so a field the caller can't
109
+ * read can't be used to filter or sort either. Recurses through relations and
110
+ * AND/OR/NOT. Runs on the CLIENT args, before the trusted author-defined
111
+ * conditions are injected, so it never blocks the framework's own conditions.
112
+ */
113
+ private assertFilterFieldsReadable;
114
+ /** Recursively assert no omitted field is referenced in a where/orderBy clause. */
115
+ private assertClauseFieldsReadable;
116
+ private throwUnreadableFilterField;
105
117
  private applyWhereConditions;
106
118
  /**
107
119
  * Enforces permission `conditions` on `create` / `createMany`.
@@ -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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"}
@@ -136,6 +136,11 @@ let PrismaService = exports.PrismaService = PrismaService_1 = class PrismaServic
136
136
  await this.enforceNestedWrites(String(propKey), params.data, user, userRole);
137
137
  }
138
138
  else {
139
+ // Reject a client where/orderBy/distinct that references a
140
+ // field this role can't read (input counterpart to output
141
+ // field omission). Runs on the client input, BEFORE the
142
+ // trusted ABAC conditions are injected below.
143
+ this.assertFilterFieldsReadable(String(propKey), userRole, params);
139
144
  params = this.applyWhereConditions(String(propKey), userRole, params, user, methodKey);
140
145
  // An update can re-point a foreign key (scalar column);
141
146
  // authorize those changes with the same connect rules as create.
@@ -342,6 +347,70 @@ let PrismaService = exports.PrismaService = PrismaService_1 = class PrismaServic
342
347
  * @returns The modified query arguments with the appropriate `where` conditions applied.
343
348
  * @throws ForbiddenException if no valid `where` conditions are present after applying permissions.
344
349
  */
350
+ /**
351
+ * Reject a client `where` / `orderBy` / `distinct` that references a field
352
+ * this role cannot read (per `@Role` / `getFieldsToOmit`) — the input-side
353
+ * counterpart to `applyFieldOmission` on output, so a field the caller can't
354
+ * read can't be used to filter or sort either. Recurses through relations and
355
+ * AND/OR/NOT. Runs on the CLIENT args, before the trusted author-defined
356
+ * conditions are injected, so it never blocks the framework's own conditions.
357
+ */
358
+ assertFilterFieldsReadable(modelName, userRole, args) {
359
+ if (!args || typeof args !== 'object')
360
+ return;
361
+ if (args.where)
362
+ this.assertClauseFieldsReadable(modelName, userRole, args.where, 'where');
363
+ if (args.orderBy)
364
+ this.assertClauseFieldsReadable(modelName, userRole, args.orderBy, 'orderBy');
365
+ if (args.distinct) {
366
+ const omit = new Set(this.getFieldsToOmit(modelName, userRole));
367
+ for (const field of [].concat(args.distinct)) {
368
+ if (omit.has(field))
369
+ this.throwUnreadableFilterField(modelName, field, userRole, 'distinct');
370
+ }
371
+ }
372
+ }
373
+ /** Recursively assert no omitted field is referenced in a where/orderBy clause. */
374
+ assertClauseFieldsReadable(modelName, userRole, clause, kind) {
375
+ if (Array.isArray(clause)) {
376
+ for (const c of clause)
377
+ this.assertClauseFieldsReadable(modelName, userRole, c, kind);
378
+ return;
379
+ }
380
+ if (!clause || typeof clause !== 'object')
381
+ return;
382
+ const omit = new Set(this.getFieldsToOmit(modelName, userRole));
383
+ for (const key of Object.keys(clause)) {
384
+ const value = clause[key];
385
+ // Logical grouping — recurse on the same model.
386
+ if (key === 'AND' || key === 'OR' || key === 'NOT') {
387
+ this.assertClauseFieldsReadable(modelName, userRole, value, kind);
388
+ continue;
389
+ }
390
+ // Direct reference to a field the role can't read.
391
+ if (omit.has(key)) {
392
+ this.throwUnreadableFilterField(modelName, key, userRole, kind);
393
+ }
394
+ // Relation — recurse into the related model (unwrapping list/one filters).
395
+ const relation = this.getRelation(modelName, key);
396
+ if (relation) {
397
+ const wrappers = ['some', 'every', 'none', 'is', 'isNot'];
398
+ const wrapped = value && typeof value === 'object' ? wrappers.filter((w) => w in value) : [];
399
+ if (wrapped.length) {
400
+ for (const w of wrapped)
401
+ this.assertClauseFieldsReadable(relation.model, userRole, value[w], kind);
402
+ }
403
+ else {
404
+ this.assertClauseFieldsReadable(relation.model, userRole, value, kind);
405
+ }
406
+ }
407
+ // Otherwise: a readable scalar field. Its value is an operator object
408
+ // (where) or 'asc'/'desc' (orderBy) — not a field map, so no recursion.
409
+ }
410
+ }
411
+ throwUnreadableFilterField(modelName, field, userRole, kind) {
412
+ throw new common_1.HttpException(`Cannot ${kind === 'orderBy' ? 'order by' : 'filter by'} '${modelName}.${field}' — not readable by role ${userRole}.`, common_1.HttpStatus.FORBIDDEN);
413
+ }
345
414
  applyWhereConditions(modelName, userRole, args, user, action) {
346
415
  if (!args) {
347
416
  args = {};
package/docs/graphql.md CHANGED
@@ -1,72 +1,134 @@
1
1
  # GraphQL (read API)
2
2
 
3
- AppX Core can expose a **read-only GraphQL API** generated from your Prisma schema. Queries run through the same ABAC proxy as REST, so row-level filtering and field omission apply automatically.
3
+ AppX Core exposes a **read-only GraphQL API** generated from your Prisma schema. Every query runs through the same ABAC proxy as REST, so row-level filtering, field omission, and nested-relation filtering all apply automatically — a caller only ever receives rows and fields their role is permitted to read.
4
4
 
5
- > **Scope:** only **read** queries are available today — `findAll<Model>s`, `findOne<Model>`, `findFirst<Model>`. Writes (create/update/delete mutations) are **not available yet**; see [limitations.md](./limitations.md). Use the REST CRUD endpoints for writes.
5
+ > **Scope:** **read only** — `find`, `get`, `count`. Writes (create/update/delete) are **not** exposed; use the REST CRUD endpoints. See [limitations.md](./limitations.md).
6
+
7
+ > **Recommended for reads.** For querying, filtering, and **pagination** (native `take` / `skip` / `cursor`) prefer GraphQL over the generated CRUD list endpoint — one request can select several models, alias repeated queries, and pull nested relations in a single round-trip. Enable it on your main queryable models (one line per module, below); keep REST for writes.
6
8
 
7
9
  ---
8
10
 
9
- ## Queries
11
+ ## The endpoint
12
+
13
+ The GraphQL server is mounted at **`POST /graphql`** (Apollo; open `GET /graphql` in a browser for the Apollo Sandbox). New projects (`appx-core create`) get it wired in `app.module` by default. To add it to an existing project, register the module once:
14
+
15
+ ```ts
16
+ import { GraphqlModule } from '@appxdigital/appx-core';
17
+
18
+ @Module({ imports: [ /* … */ GraphqlModule ] })
19
+ export class AppModule {}
20
+ ```
21
+
22
+ The server is always valid even before any model is exposed (it ships a trivial `_appxCore` root field), so `/graphql` is live from the start.
10
23
 
11
- For each model, the generated schema provides:
24
+ ---
12
25
 
13
- | Query | Returns |
14
- |---|---|
15
- | `findAll<Model>s(...)` | a list |
16
- | `findFirst<Model>(...)` | the first match, or `null` |
17
- | `findOne<Model>(where: {...})` | a single record, or `null` |
26
+ ## Exposing a model (opt-in, per module — one line)
27
+
28
+ GraphQL is **off per model** until you opt in. Every generated module ships a commented, copy-paste-ready opt-in; enabling a model is one import + one provider line, with **no resolver file to write or maintain**:
29
+
30
+ ```ts
31
+ import { Module } from '@nestjs/common';
32
+ import { PrismaModule } from '../../prisma/prisma.module';
33
+ import { AlbumController } from './album.controller';
34
+ import { AlbumService } from './album.service';
35
+ import { AlbumResolver } from '../../generated/album/graphql'; // generated by `appx-core generate`
36
+
37
+ @Module({
38
+ imports: [PrismaModule],
39
+ controllers: [AlbumController],
40
+ providers: [
41
+ AlbumService,
42
+ AlbumResolver, // ← add = exposed; remove = not exposed
43
+ ],
44
+ })
45
+ export class AlbumModule {}
46
+ ```
47
+
48
+ `AlbumResolver` is a ready-to-register resolver class emitted by the deploy-safe pass (`appx-core generate`) at `src/generated/album/graphql.ts` — regenerated from the schema, never hand-edited. (The same file exports the underlying `AlbumGraphql` bundle if you'd rather build the resolver yourself via the exported `CoreGraphqlResolver`.)
49
+
50
+ ---
51
+
52
+ ## Queries — `find` / `get` / `count`
53
+
54
+ Each exposed model contributes **one root field** (its camelCase name) whose fields are the three read operations. Naming is uniform across every model, so it's predictable:
55
+
56
+ | Field | Prisma | Returns |
57
+ |---|---|---|
58
+ | `find(where, orderBy, take, skip, …)` | `findMany` | a list |
59
+ | `get(where, …)` | `findFirst` | a single record, or `null` |
60
+ | `count(where)` | `count` | `Int` |
18
61
 
19
62
  ```graphql
20
63
  query {
21
- findAllProjects(where: { status: "active" }) {
22
- id
23
- name
64
+ album {
65
+ find(where: { year: { equals: 2024 } }, take: 20) { id title }
66
+ get(where: { id: { equals: 1 } }) { id title }
67
+ count(where: { year: { equals: 2024 } })
24
68
  }
25
69
  }
26
70
  ```
27
71
 
28
- These route through `PrismaService`, so the caller's role and the rules in `permissions.config.ts` are enforced exactly as they are for REST reads (see [permissions.md](./permissions.md) and [data-access.md](./data-access.md)). A user only ever receives rows and fields their role is permitted to read.
72
+ Because `find`/`get`/`count` live on the per-model namespace (not the root), the names repeat freely across models and never collide.
29
73
 
30
- ---
74
+ ### Multiple things in one request + aliases
31
75
 
32
- ## Nested queries
76
+ A single query can select many models at once, and aliases let you repeat an operation with different arguments — all in one round-trip:
33
77
 
34
- You can select related models inline, to any depth:
78
+ ```graphql
79
+ query {
80
+ album {
81
+ recent: find(where: { year: { equals: 2024 } }) { id title }
82
+ classics: find(where: { year: { lt: 1990 } }) { id title }
83
+ total: count
84
+ }
85
+ artist { get(where: { id: { equals: 7 } }) { name } }
86
+ }
87
+ ```
88
+
89
+ Each selected field resolves independently through the proxy (its own authorized query), so multiplexing never weakens enforcement. `count` is filtered too — it counts only the rows the caller may read, never the table total.
90
+
91
+ ### Nested relations & children
92
+
93
+ Select related models inline, to any depth. The GraphQL selection is compiled into a single Prisma `select`, so relations come back in the same authorized query (no N+1):
35
94
 
36
95
  ```graphql
37
96
  query {
38
- findAllProjects {
39
- id
40
- name
41
- tasks {
97
+ album {
98
+ find {
42
99
  title
43
- assignee {
44
- name
45
- }
100
+ tracks { title } # to-many child
101
+ artist { name } # to-one relation
46
102
  }
47
103
  }
48
104
  }
49
105
  ```
50
106
 
51
- Nested selections are **ABAC-filtered** just like the top level.
107
+ ---
108
+
109
+ ## What the caller can and cannot see (ABAC)
52
110
 
53
- > **Caveat — inaccessible nested data comes back empty, not as an error.** If the current user isn't allowed to read a related record, it is simply filtered out:
54
- > - a **to-many** relation (`tasks`, `members`, …) returns an **empty list** for the parts the user can't see;
55
- > - an **inaccessible to-one** relation causes the **parent row to be omitted** from the result (you can't see a row whose related record you aren't allowed to read).
56
- >
57
- > No exception is thrown — the data you're not entitled to just isn't there. Design your queries and UI to treat "empty" as "not visible to you", not "does not exist".
111
+ All three operations route through `PrismaService.getModelDelegate`, so the rules in `permissions.config.ts` apply exactly as they do for REST reads (see [permissions.md](./permissions.md)). Concretely:
58
112
 
59
- ---
113
+ - **Unauthorized rows are not returned.** `find` returns only rows the role may read; `get` returns `null` for a row the caller can't access even if asked for by unique field; `count` counts only accessible rows.
114
+ - **Restricted fields are omitted.** A column the role can't read (`/// @Role(...)`) comes back **`null`** even though the GraphQL type exposes it — at the top level *and* nested. (`prisma-nestjs-graphql` types every scalar; the proxy is what strips the values.)
115
+ - **Inaccessible nested records are filtered.** If a nested relation points at a record the caller can't read, that relation is filtered out — a to-many yields only the visible rows; an inaccessible to-one drops the parent row. You can hold access to a record while a record it links to is withheld.
116
+ - **You can only filter/sort by fields you can read.** A `where` / `orderBy` / `distinct` that references a field the role can't read (`@Role`) is rejected with `403` — top level or nested — so a hidden column can't be used to filter or order either.
117
+ - **Anonymous callers are `GUEST`.** With no valid `Authorization: Bearer <jwt>` (or session), the request is a GUEST; a model with no GUEST rule is denied.
60
118
 
61
- ## Field-level visibility
119
+ > Because relation records and fields are filtered by ABAC, "empty / null" means "not visible to you", not "does not exist". Design queries and UI accordingly.
62
120
 
63
- Columns annotated `/// @Role(...)` follow the same rules as everywhere else: a role that can't read a field won't receive it in the GraphQL response, at the top level and nested. See [permissions.md](./permissions.md#field-level-control).
121
+ Always let the generated resolver serve the data — a custom resolver that reaches the **raw** Prisma client bypasses ABAC.
64
122
 
65
123
  ---
66
124
 
67
125
  ## Not available yet
68
126
 
69
127
  - **Mutations** (create/update/delete) — use REST.
70
- - **`aggregate`** — not ready; see [limitations.md](./limitations.md).
128
+ - **Full `aggregate`** (sum/avg/min/max/groupBy) — only `count` is exposed today.
129
+
130
+ ---
131
+
132
+ ## After changing the schema
71
133
 
72
- Always let the generated resolvers (which go through `PrismaService`) serve the data — a custom resolver that reaches the raw Prisma client would not get ABAC.
134
+ Re-run `appx-core generate`. It refreshes the Prisma client, GraphQL types, DTO bases, and the per-model `graphql.ts` bundles under `src/generated/**`. Your module opt-in lines are untouched.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appxdigital/appx-core",
3
- "version": "0.1.127",
3
+ "version": "0.1.128-beta.1",
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",
@@ -59,6 +59,7 @@
59
59
  "@adminjs/nestjs": "~7.0.0",
60
60
  "@adminjs/passwords": "~4.0.0",
61
61
  "@adminjs/prisma": "~5.0.4",
62
+ "@apollo/server": "^4.11.3",
62
63
  "@as-integrations/express5": "~1.1.2",
63
64
  "@aws-sdk/client-s3": "~3.850.0",
64
65
  "@aws-sdk/s3-request-presigner": "~3.850.0",
@@ -1,8 +0,0 @@
1
- /**
2
- * Scaffold the (read-only) GraphQL resolver file for a single model (once;
3
- * never overwritten). Imports the prisma-nestjs-graphql artifacts under
4
- * `src/generated/<folder>/`, so the deploy-safe pass (`prisma generate`) must
5
- * have produced them first.
6
- */
7
- export declare function scaffoldResolver(modelName: string): void;
8
- //# sourceMappingURL=generate-resolvers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-resolvers.d.ts","sourceRoot":"","sources":["../../src/config/generate-resolvers.ts"],"names":[],"mappings":"AA8CA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIxD"}
@@ -1,80 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.scaffoldResolver = void 0;
27
- const path = __importStar(require("path"));
28
- const utils_1 = require("./utils");
29
- const outputPath = path.join(process.cwd(), 'src/modules');
30
- /**
31
- * Generic resolver template for a given model
32
- * @param model
33
- * @param folder
34
- */
35
- const genericResolverTemplate = (model, folder) => `import { Resolver } from '@nestjs/graphql';
36
- import { GenericResolverFactory, PrismaService } from '@appxdigital/appx-core';
37
- import { ${model} } from '../../generated/${folder}/${folder}.model';
38
- import { ${model}CreateInput } from '../../generated/${folder}/${folder}-create.input';
39
- import { ${model}UpdateInput } from '../../generated/${folder}/${folder}-update.input';
40
- import { ${model}WhereInput } from '../../generated/${folder}/${folder}-where.input';
41
- import { ${model}WhereUniqueInput } from '../../generated/${folder}/${folder}-where-unique.input';
42
- import { FindMany${model}Args } from '../../generated/${folder}/find-many-${folder}.args';
43
- import { ${model}AggregateArgs } from '../../generated/${folder}/${folder}-aggregate.args';
44
- import { Aggregate${model} } from '../../generated/${folder}/aggregate-${folder}.output';
45
- import { CreateMany${model}Args } from '../../generated/${folder}/create-many-${folder}.args';
46
- import { ${model}CreateManyInput } from '../../generated/${folder}/${folder}-create-many.input';
47
-
48
- const ${model}GenericResolver = GenericResolverFactory(
49
- '${model}',
50
- ${model},
51
- ${model}CreateInput,
52
- ${model}UpdateInput,
53
- ${model}WhereInput,
54
- ${model}WhereUniqueInput,
55
- FindMany${model}Args,
56
- ${model}AggregateArgs,
57
- Aggregate${model},
58
- CreateMany${model}Args,
59
- ${model}CreateManyInput,
60
- );
61
-
62
- @Resolver(() => ${model})
63
- export class ${model}Resolver extends ${model}GenericResolver {
64
- constructor(prisma: PrismaService) {
65
- super(prisma);
66
- }
67
- }
68
- `;
69
- /**
70
- * Scaffold the (read-only) GraphQL resolver file for a single model (once;
71
- * never overwritten). Imports the prisma-nestjs-graphql artifacts under
72
- * `src/generated/<folder>/`, so the deploy-safe pass (`prisma generate`) must
73
- * have produced them first.
74
- */
75
- function scaffoldResolver(modelName) {
76
- const folder = (0, utils_1.modelFolder)(modelName);
77
- const resolverPath = path.join(outputPath, folder, `${folder}.resolver.ts`);
78
- (0, utils_1.createFileIfNotExists)(resolverPath, genericResolverTemplate(modelName, folder));
79
- }
80
- exports.scaffoldResolver = scaffoldResolver;