@nestjs-cms/core 0.0.0-stage → 0.1.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.
Files changed (63) hide show
  1. package/.env.example +21 -0
  2. package/README.md +341 -2
  3. package/dist/src/api-key.guard.d.ts +4 -0
  4. package/dist/src/api-key.guard.js +29 -0
  5. package/dist/src/api-key.guard.js.map +1 -0
  6. package/dist/src/config.d.ts +43 -0
  7. package/dist/src/config.js +47 -0
  8. package/dist/src/config.js.map +1 -0
  9. package/dist/src/database/create-data-source.d.ts +4 -0
  10. package/dist/src/database/create-data-source.js +23 -0
  11. package/dist/src/database/create-data-source.js.map +1 -0
  12. package/dist/src/error.filter.d.ts +6 -0
  13. package/dist/src/error.filter.js +54 -0
  14. package/dist/src/error.filter.js.map +1 -0
  15. package/dist/src/index.d.ts +12 -0
  16. package/dist/src/index.js +23 -0
  17. package/dist/src/index.js.map +1 -0
  18. package/dist/src/resource/query.d.ts +15 -0
  19. package/dist/src/resource/query.js +147 -0
  20. package/dist/src/resource/query.js.map +1 -0
  21. package/dist/src/resource/resource-record.store.d.ts +7 -0
  22. package/dist/src/resource/resource-record.store.js +39 -0
  23. package/dist/src/resource/resource-record.store.js.map +1 -0
  24. package/dist/src/resource/resource.controller.d.ts +46 -0
  25. package/dist/src/resource/resource.controller.js +165 -0
  26. package/dist/src/resource/resource.controller.js.map +1 -0
  27. package/dist/src/resource/resource.loader.d.ts +2 -0
  28. package/dist/src/resource/resource.loader.js +43 -0
  29. package/dist/src/resource/resource.loader.js.map +1 -0
  30. package/dist/src/resource/resource.module.d.ts +16 -0
  31. package/dist/src/resource/resource.module.js +70 -0
  32. package/dist/src/resource/resource.module.js.map +1 -0
  33. package/dist/src/resource/resource.registry.d.ts +21 -0
  34. package/dist/src/resource/resource.registry.js +74 -0
  35. package/dist/src/resource/resource.registry.js.map +1 -0
  36. package/dist/src/resource/resource.service.d.ts +60 -0
  37. package/dist/src/resource/resource.service.js +150 -0
  38. package/dist/src/resource/resource.service.js.map +1 -0
  39. package/dist/src/resource/resource.types.d.ts +33 -0
  40. package/dist/src/resource/resource.types.js +7 -0
  41. package/dist/src/resource/resource.types.js.map +1 -0
  42. package/dist/src/resource/validation.d.ts +3 -0
  43. package/dist/src/resource/validation.js +19 -0
  44. package/dist/src/resource/validation.js.map +1 -0
  45. package/dist/src/setup.d.ts +3 -0
  46. package/dist/src/setup.js +16 -0
  47. package/dist/src/setup.js.map +1 -0
  48. package/dist/src/storage/attachment.entity.d.ts +23 -0
  49. package/dist/src/storage/attachment.entity.js +37 -0
  50. package/dist/src/storage/attachment.entity.js.map +1 -0
  51. package/dist/src/storage/attachment.service.d.ts +46 -0
  52. package/dist/src/storage/attachment.service.js +190 -0
  53. package/dist/src/storage/attachment.service.js.map +1 -0
  54. package/dist/src/storage/attachment.types.d.ts +5 -0
  55. package/dist/src/storage/attachment.types.js +3 -0
  56. package/dist/src/storage/attachment.types.js.map +1 -0
  57. package/dist/src/storage/s3.service.d.ts +8 -0
  58. package/dist/src/storage/s3.service.js +65 -0
  59. package/dist/src/storage/s3.service.js.map +1 -0
  60. package/docs/api.md +166 -0
  61. package/docs/operations.md +166 -0
  62. package/docs/resources.md +151 -0
  63. package/package.json +82 -4
package/.env.example ADDED
@@ -0,0 +1,21 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ HOST=127.0.0.1
4
+ CORS_ORIGINS=
5
+ # Optional shared API key for development/integration; rights/auth module is out of scope.
6
+ ADMIN_API_KEY=
7
+ DB_TYPE=postgres
8
+ DB_HOST=127.0.0.1
9
+ DB_PORT=5432
10
+ DB_USERNAME=cms
11
+ DB_PASSWORD=cms_dev_password
12
+ DB_DATABASE=cms
13
+ # Dev only. Use migrations in production.
14
+ DB_SYNCHRONIZE=true
15
+ S3_REGION=us-east-1
16
+ S3_BUCKET=cms-attachments
17
+ S3_ENDPOINT=http://127.0.0.1:9000
18
+ S3_FORCE_PATH_STYLE=true
19
+ S3_ACCESS_KEY_ID=minioadmin
20
+ S3_SECRET_ACCESS_KEY=minioadmin123
21
+ ATTACHMENT_MAX_BYTES=20971520
package/README.md CHANGED
@@ -1,3 +1,342 @@
1
- # Temporary Holding Version
1
+ # @nestjs-cms/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Add a resource-driven CMS backend to a NestJS application. Define each resource in one file; the module discovers it and provides validated CRUD APIs, filters, lifecycle hooks, and S3-backed attachments.
4
+
5
+ ## Quick start
6
+
7
+ Requires Node.js 22+, npm, and a PostgreSQL database. The package supports NestJS 11 with the Express adapter.
8
+
9
+ ### 1. Create a NestJS project and install the package
10
+
11
+ ```sh
12
+ npx @nestjs/cli@11 new nestjs-cms-demo --package-manager npm
13
+ cd nestjs-cms-demo
14
+ npm install @nestjs-cms/core
15
+ ```
16
+
17
+ The npm package includes compiled JavaScript and TypeScript declarations. Imports use `"@nestjs-cms/core"`.
18
+
19
+ The package includes Zod, TypeORM, PostgreSQL/MySQL drivers, dotenv, and the S3 client. The NestJS project supplies the NestJS runtime dependencies.
20
+
21
+ ### 2. Create a resource
22
+
23
+ ```sh
24
+ mkdir -p src/resources
25
+ ```
26
+
27
+ Create `src/resources/pages.ts`:
28
+
29
+ ```ts
30
+ import { defineResource, z } from "@nestjs-cms/core";
31
+
32
+ export default defineResource({
33
+ name: "pages",
34
+ schema: z
35
+ .object({
36
+ title: z.string().trim().min(1).max(200),
37
+ status: z.enum(["draft", "published"]).default("draft"),
38
+ })
39
+ .strict(),
40
+ columns: {
41
+ title: { type: String, length: 200 },
42
+ status: { type: String, length: 20 },
43
+ },
44
+ filterable: ["title", "status"],
45
+ sortable: ["title", "createdAt"],
46
+ });
47
+ ```
48
+
49
+ Keep the folder inside `src` so the standard Nest compiler builds it into `dist/resources`. Each additional resource requires only one new file in this folder. No resource index or per-resource registration is needed.
50
+
51
+ ### 3. Import the CMS module once
52
+
53
+ Add `ResourceModule.forRoot(...)` to the `imports` array in `src/app.module.ts`:
54
+
55
+ ```ts
56
+ import { Module } from "@nestjs/common";
57
+ import { join } from "node:path";
58
+ import { ResourceModule } from "@nestjs-cms/core";
59
+
60
+ @Module({
61
+ imports: [
62
+ ResourceModule.forRoot({ resourcesPath: join(__dirname, "resources") }),
63
+ ],
64
+ })
65
+ export class AppModule {}
66
+ ```
67
+
68
+ You can keep your existing controllers, providers, and other imports. The module's API-key guard and error filter apply only to CMS routes.
69
+
70
+ ### 4. Configure `.env`
71
+
72
+ Create `.env` in the new project's root:
73
+
74
+ ```dotenv
75
+ NODE_ENV=development
76
+ PORT=3000
77
+ DB_TYPE=postgres
78
+ DB_HOST=127.0.0.1
79
+ DB_PORT=5432
80
+ DB_USERNAME=cms
81
+ DB_PASSWORD=cms_dev_password
82
+ DB_DATABASE=cms
83
+ DB_SYNCHRONIZE=true
84
+ ADMIN_API_KEY=
85
+ CORS_ORIGINS=
86
+ ```
87
+
88
+ Use credentials for an existing PostgreSQL database. The package loads `.env` automatically. With `DB_SYNCHRONIZE=true`, it creates the resource tables in that database during startup. Use migrations instead in production.
89
+
90
+ If you need a local database and port 5432 is available:
91
+
92
+ ```sh
93
+ docker run --name nestjs-cms-demo-postgres -d \
94
+ -e POSTGRES_USER=cms -e POSTGRES_PASSWORD=cms_dev_password -e POSTGRES_DB=cms \
95
+ -p 127.0.0.1:5432:5432 postgres:17-alpine
96
+ ```
97
+
98
+ Wait for PostgreSQL to be ready before starting the application. The example above supports plain resource CRUD. **Attachments also require working S3/MinIO storage and an attachment field in the resource definition.**
99
+
100
+ ### 4a. Configure storage for attachments
101
+
102
+ For local MinIO, add these settings to your application's `.env`:
103
+
104
+ ```dotenv
105
+ S3_REGION=us-east-1
106
+ S3_BUCKET=cms-attachments
107
+ S3_ENDPOINT=http://127.0.0.1:9000
108
+ S3_FORCE_PATH_STYLE=true
109
+ S3_ACCESS_KEY_ID=minioadmin
110
+ S3_SECRET_ACCESS_KEY=minioadmin123
111
+ ATTACHMENT_MAX_BYTES=20971520
112
+ ```
113
+
114
+ These credentials match this library's development Compose setup. From the `nestjs-cms` repository checkout, start storage with:
115
+
116
+ ```sh
117
+ docker compose up -d --build minio minio-init
118
+ ```
119
+
120
+ Wait for MinIO to become healthy and `minio-init` to finish successfully; it creates the private `cms-attachments` bucket. The npm package does not start storage or include the Compose setup. You can also use an existing MinIO server: supply its endpoint and credentials and create the bucket beforehand. The endpoint above assumes the application runs on your host machine.
121
+
122
+ For AWS S3 instead, use your bucket's region and name, omit `S3_ENDPOINT`, and set `S3_FORCE_PATH_STYLE=false`. Supply both credential variables, or omit both when using an IAM role. See [AWS S3 configuration](docs/operations.md#aws-s3-configuration) for permissions.
123
+
124
+ In `src/resources/pages.ts`, add this property to the existing `defineResource({...})` object:
125
+
126
+ ```ts
127
+ attachments: [
128
+ { field: "cover", mimeTypes: ["image/jpeg", "image/png", "image/webp"] },
129
+ ],
130
+ ```
131
+
132
+ Attachment fields are declared separately from the Zod schema and database columns. After restarting the application and creating a page, upload a file using its record ID:
133
+
134
+ ```sh
135
+ curl -X POST http://localhost:3000/api/pages/RECORD_ID/attachments/cover \
136
+ -F 'file=@/absolute/path/to/cover.png'
137
+ ```
138
+
139
+ Replace `RECORD_ID` and the file path with your values. If `ADMIN_API_KEY` is configured, include the `X-API-Key` header. Uploads and downloads always pass through the backend attachment API; the browser never connects directly to S3/MinIO.
140
+
141
+ Without working object storage, the application can start and plain resource CRUD still works. Storage is contacted when an attachment operation needs it: uploads fail with HTTP 503 (`Object storage upload unavailable`), and downloads of existing attachments fail with HTTP 503 (`Attachment content unavailable`). There is no local-filesystem fallback. Omitting the S3 settings does not disable attachments; the client uses the default region/bucket and AWS credential chain, which only works if that environment already has access to the matching storage.
142
+
143
+ ### 5. Start the application
144
+
145
+ ```sh
146
+ npm run start:dev
147
+ ```
148
+
149
+ ```sh
150
+ curl http://localhost:3000/api/_resources
151
+ curl -X POST http://localhost:3000/api/pages \
152
+ -H 'Content-Type: application/json' \
153
+ -d '{"title":"About us"}'
154
+ curl http://localhost:3000/api/pages
155
+ ```
156
+
157
+ After adding or removing resource files, rebuild/restart the application. The standard Nest CLI TypeScript build emits the resource files separately; bundlers require additional configuration to preserve those files for runtime discovery.
158
+
159
+ For production, run the host project's `npm run build` and `npm run start:prod`, disable schema synchronization, and apply reviewed migrations. The library does not run or install its demo migrations in your application.
160
+
161
+ ### Optional: CORS and shutdown hooks
162
+
163
+ Same-origin frontends do not require CORS. To apply `CORS_ORIGINS` for a separate frontend and enable graceful shutdown, call the helper in your application's existing `src/main.ts`:
164
+
165
+ ```ts
166
+ import { configureCmsApp } from "@nestjs-cms/core";
167
+
168
+ // After NestFactory.create(AppModule), before app.listen(...):
169
+ configureCmsApp(app);
170
+ ```
171
+
172
+ Set `CORS_ORIGINS=http://localhost:5173`, for example. Your host application still controls its listening port, bind address, and bootstrap process. If it already manages CORS and shutdown hooks, keep that configuration instead.
173
+
174
+ ## Migrating from `nestjs-cms`
175
+
176
+ The backend package is now named `@nestjs-cms/core`. Once the scoped package is
177
+ published, existing applications can switch with:
178
+
179
+ ```sh
180
+ npm uninstall nestjs-cms
181
+ npm install @nestjs-cms/core
182
+ ```
183
+
184
+ Replace imports from `"nestjs-cms"` with `"@nestjs-cms/core"`, including type
185
+ imports. The API is unchanged by this rename. The admin package remains
186
+ `@nestjs-cms/admin`; the GitHub repository remains `kongyim/nestjs-cms`.
187
+
188
+ ## Install a local package
189
+
190
+ From this library's checkout:
191
+
192
+ ```sh
193
+ npm ci
194
+ npm pack
195
+ ```
196
+
197
+ This builds `nestjs-cms-core-0.1.0.tgz`, including JavaScript and TypeScript declarations. In the new NestJS project, replace the npm installation step with:
198
+
199
+ ```sh
200
+ npm install /absolute/path/to/nestjs-cms-core-0.1.0.tgz
201
+ ```
202
+
203
+ The package excludes the demo application, demo resources, demo migrations, tests, and local secrets. Packing does not publish anything to a registry.
204
+
205
+ ## Install from GitHub (optional)
206
+
207
+ To use an unpublished revision, install from GitHub instead of npm. This requires
208
+ Git and a GitHub SSH key with access to the repository:
209
+
210
+ ```sh
211
+ npm install git+ssh://git@github.com/kongyim/nestjs-cms.git
212
+ ```
213
+
214
+ The package's `prepare` script builds TypeScript during Git installation. Keep
215
+ npm lifecycle scripts enabled. Append `#<tag-or-commit>` to the Git URL to select
216
+ an existing revision. Imports still use `"@nestjs-cms/core"`.
217
+
218
+ ### Troubleshooting: Cannot find module `@nestjs-cms/core`
219
+
220
+ Run `npm ls @nestjs-cms/core` in the consuming application and check that
221
+ `node_modules/@nestjs-cms/core/dist/src/index.js` and `index.d.ts` exist. To reinstall
222
+ the published package, stop the development server and run:
223
+
224
+ ```sh
225
+ npm uninstall @nestjs-cms/core
226
+ npm install @nestjs-cms/core
227
+ npm run start:dev
228
+ ```
229
+
230
+ For Git installations, missing compiled files can indicate an older revision or
231
+ a failed `prepare` script. Reinstall with `--foreground-scripts` to see build
232
+ output, or use a locally packed package as described above.
233
+
234
+ ## Publishing updates to npm
235
+
236
+ For the first release under the new name, run `npm run release:check` and
237
+ `npm run release` from the repository root. Version `0.1.0` can be used because
238
+ `@nestjs-cms/core` is a separate npm package from `nestjs-cms` (provided that
239
+ version has not already been published under the new name).
240
+
241
+ After confirming `npm view @nestjs-cms/core version` returns the new release,
242
+ the owner of the old package can mark it deprecated:
243
+
244
+ ```sh
245
+ npm deprecate 'nestjs-cms@*' 'Renamed to @nestjs-cms/core. Install @nestjs-cms/core and update your imports.'
246
+ ```
247
+
248
+ This keeps existing versions available while directing users to the new package.
249
+ For subsequent releases, follow the steps below.
250
+
251
+
252
+ Run the following commands in the package you changed: the repository root for
253
+ `@nestjs-cms/core`, or `packages/admin` for `@nestjs-cms/admin`. The packages have
254
+ independent versions; release only the packages you changed.
255
+
256
+ ```sh
257
+ npm ci
258
+ npm run release:check
259
+ npm run version:patch
260
+ npm run release
261
+ ```
262
+
263
+ - `release:check` runs tests, builds the package, and previews the files to publish
264
+ without uploading anything. Review that file list before continuing. For backend
265
+ changes involving the database or storage, also run `npm run test:integration`
266
+ with the required services running.
267
+ - `version:patch` updates `package.json` and `package-lock.json` automatically
268
+ (for example, `0.1.0` → `0.1.1`). No manual version editing is needed.
269
+ Use `version:minor` for a feature release (`0.1.0` → `0.2.0`) or
270
+ `version:major` for a major release (`0.1.0` → `1.0.0`). During `0.x`
271
+ development, document breaking changes explicitly and use a minor release;
272
+ from `1.0.0` onward, use a major release for breaking changes.
273
+ - `release` publishes the current version publicly. Tests run again through
274
+ `prepublishOnly`, and the existing packaging hook builds the package before
275
+ upload. If needed, run `npm login --auth-type=web` first and complete npm's
276
+ authentication prompts.
277
+
278
+ Every published update needs a new version, including README-only updates on npm.
279
+ A published name/version cannot be reused. These scripts do not create Git commits
280
+ or tags: review and commit the code, README, and version/lockfile changes, then
281
+ push them to GitHub to keep the release source recorded.
282
+
283
+ If publication fails after the version bump, fix the problem and run
284
+ `npm run release` again without bumping again, provided that version was not
285
+ published. If the outcome is unclear, check `npm view <package-name>@<version> version`
286
+ first. The version scripts only change local files; `release:check` does not bump
287
+ versions, and `release` does not bump automatically.
288
+
289
+ See [npm version](https://docs.npmjs.com/cli/commands/npm-version/) for version
290
+ updates and [npm publish](https://docs.npmjs.com/cli/commands/npm-publish/) for
291
+ publication rules.
292
+
293
+ ## Documentation
294
+
295
+ - [Resource definitions, hooks, and service methods](docs/resources.md)
296
+ - [API contract and frontend examples](docs/api.md)
297
+ - [Environment, databases, storage, and deployment](docs/operations.md)
298
+
299
+ `ResourceService` is exported for use in other application services. It provides CRUD and `createAttachment`, `findAttachment`, `downloadAttachment`, and `removeAttachment`. All browser attachment traffic goes through the backend API.
300
+
301
+ User login, roles, and resource rights are not implemented. Setting `ADMIN_API_KEY` requires `X-API-Key` on CMS requests; production currently requires a key of at least 32 characters. This is an integration mechanism, not a full authentication system.
302
+
303
+ ## Develop this library
304
+
305
+ The repository also contains a demo application with `categories` and `articles`. These instructions are for library contributors, not package consumers:
306
+
307
+ ```sh
308
+ npm ci
309
+ cp .env.example .env
310
+ docker compose up -d --build
311
+ # Wait for postgres/minio to be healthy and minio-init to exit with code 0.
312
+ npm run seed
313
+ npm run dev
314
+ ```
315
+
316
+ The first MinIO build compiles a pinned source release and takes longer than subsequent starts. The demo API runs at `http://localhost:3000/api`, and the MinIO console at `http://localhost:9001`.
317
+
318
+ ```sh
319
+ npm run format
320
+ npm run format:check
321
+ npm run build
322
+ npm test
323
+ npm run test:integration
324
+ # Optional MySQL verification:
325
+ docker compose --profile mysql up -d mysql
326
+ DB_TYPE=mysql DB_PORT=3307 npm run test:integration
327
+ ```
328
+
329
+ Integration tests use the dedicated `cms_test` database and real MinIO services. The repository's VS Code settings enable formatting on save when the recommended Prettier extension is installed. These editor settings are not installed into consuming applications.
330
+
331
+ ## Scope
332
+
333
+ The package implements resource discovery, CRUD, filters, transactional hooks, and backend-managed attachments. Import/export, image resizing, encrypted/hashed fields, automatic relation expansion, and resource permissions are not included. Multilingual and nested fields use JSON. See the [comparison with the original project](docs/resources.md#comparison-with-the-original-project) for differences from `polyu-souvenirs`.
334
+
335
+ ## Reusable admin frontend
336
+
337
+ The companion React package lives in [packages/admin](packages/admin/README.md).
338
+ It provides the portal UI, explicitly configured sections in navigation order,
339
+ nested JSON fields, deferred attachments, and inline password actions. It is
340
+ packaged independently as `@nestjs-cms/admin`; the core backend package does not
341
+ install frontend dependencies. See the package README for build/install steps,
342
+ Next.js setup, and authentication/transport adapters.
@@ -0,0 +1,4 @@
1
+ import { CanActivate, ExecutionContext } from "@nestjs/common";
2
+ export declare class ApiKeyGuard implements CanActivate {
3
+ canActivate(context: ExecutionContext): boolean;
4
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ApiKeyGuard = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const config_1 = require("./config");
13
+ let ApiKeyGuard = class ApiKeyGuard {
14
+ canActivate(context) {
15
+ if (!config_1.config.ADMIN_API_KEY)
16
+ return true;
17
+ const header = context.switchToHttp().getRequest().headers["x-api-key"];
18
+ const expected = Buffer.from(config_1.config.ADMIN_API_KEY);
19
+ const actual = Buffer.from(typeof header === "string" ? header : "");
20
+ if (actual.length !== expected.length || !(0, node_crypto_1.timingSafeEqual)(actual, expected))
21
+ throw new common_1.UnauthorizedException();
22
+ return true;
23
+ }
24
+ };
25
+ exports.ApiKeyGuard = ApiKeyGuard;
26
+ exports.ApiKeyGuard = ApiKeyGuard = __decorate([
27
+ (0, common_1.Injectable)()
28
+ ], ApiKeyGuard);
29
+ //# sourceMappingURL=api-key.guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key.guard.js","sourceRoot":"","sources":["../../src/api-key.guard.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAKwB;AACxB,6CAA8C;AAC9C,qCAAkC;AAE3B,IAAM,WAAW,GAAjB,MAAM,WAAW;IACtB,WAAW,CAAC,OAAyB;QACnC,IAAI,CAAC,eAAM,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,MAAM,GAAY,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,CACjE,WAAW,CACZ,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,eAAM,CAAC,aAAa,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACrE,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAA,6BAAe,EAAC,MAAM,EAAE,QAAQ,CAAC;YACzE,MAAM,IAAI,8BAAqB,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAZY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;GACA,WAAW,CAYvB"}
@@ -0,0 +1,43 @@
1
+ import "dotenv/config";
2
+ export declare function readConfig(input?: NodeJS.ProcessEnv): {
3
+ NODE_ENV: "development" | "test" | "production";
4
+ PORT: number;
5
+ HOST: string;
6
+ CORS_ORIGINS: string;
7
+ ADMIN_API_KEY: string;
8
+ DB_TYPE: "postgres" | "mysql";
9
+ DB_HOST: string;
10
+ DB_USERNAME: string;
11
+ DB_PASSWORD: string;
12
+ DB_DATABASE: string;
13
+ DB_SYNCHRONIZE: boolean;
14
+ S3_REGION: string;
15
+ S3_BUCKET: string;
16
+ S3_FORCE_PATH_STYLE: boolean;
17
+ ATTACHMENT_MAX_BYTES: number;
18
+ DB_PORT?: number | undefined;
19
+ S3_ENDPOINT?: string | undefined;
20
+ S3_ACCESS_KEY_ID?: string | undefined;
21
+ S3_SECRET_ACCESS_KEY?: string | undefined;
22
+ };
23
+ export declare const config: {
24
+ NODE_ENV: "development" | "test" | "production";
25
+ PORT: number;
26
+ HOST: string;
27
+ CORS_ORIGINS: string;
28
+ ADMIN_API_KEY: string;
29
+ DB_TYPE: "postgres" | "mysql";
30
+ DB_HOST: string;
31
+ DB_USERNAME: string;
32
+ DB_PASSWORD: string;
33
+ DB_DATABASE: string;
34
+ DB_SYNCHRONIZE: boolean;
35
+ S3_REGION: string;
36
+ S3_BUCKET: string;
37
+ S3_FORCE_PATH_STYLE: boolean;
38
+ ATTACHMENT_MAX_BYTES: number;
39
+ DB_PORT?: number | undefined;
40
+ S3_ENDPOINT?: string | undefined;
41
+ S3_ACCESS_KEY_ID?: string | undefined;
42
+ S3_SECRET_ACCESS_KEY?: string | undefined;
43
+ };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.readConfig = readConfig;
5
+ require("dotenv/config");
6
+ const zod_1 = require("zod");
7
+ const bool = zod_1.z.enum(["true", "false"]).transform((v) => v === "true");
8
+ const env = zod_1.z.object({
9
+ NODE_ENV: zod_1.z
10
+ .enum(["development", "test", "production"])
11
+ .default("development"),
12
+ PORT: zod_1.z.coerce.number().int().min(1).max(65535).default(3000),
13
+ HOST: zod_1.z.string().default("127.0.0.1"),
14
+ CORS_ORIGINS: zod_1.z.string().default("http://localhost:5173"),
15
+ ADMIN_API_KEY: zod_1.z.string().default(""),
16
+ DB_TYPE: zod_1.z.enum(["postgres", "mysql"]).default("postgres"),
17
+ DB_HOST: zod_1.z.string().default("127.0.0.1"),
18
+ DB_PORT: zod_1.z.coerce.number().int().positive().optional(),
19
+ DB_USERNAME: zod_1.z.string().default("cms"),
20
+ DB_PASSWORD: zod_1.z.string().default("cms_dev_password"),
21
+ DB_DATABASE: zod_1.z.string().default("cms"),
22
+ DB_SYNCHRONIZE: bool.default(false),
23
+ S3_REGION: zod_1.z.string().default("us-east-1"),
24
+ S3_BUCKET: zod_1.z.string().min(3).default("cms-attachments"),
25
+ S3_ENDPOINT: zod_1.z.url().optional(),
26
+ S3_FORCE_PATH_STYLE: bool.default(false),
27
+ S3_ACCESS_KEY_ID: zod_1.z.string().optional(),
28
+ S3_SECRET_ACCESS_KEY: zod_1.z.string().optional(),
29
+ ATTACHMENT_MAX_BYTES: zod_1.z.coerce
30
+ .number()
31
+ .int()
32
+ .positive()
33
+ .max(1024 ** 3)
34
+ .default(20 * 1024 ** 2),
35
+ });
36
+ function readConfig(input = process.env) {
37
+ const config = env.parse(input);
38
+ if (!!config.S3_ACCESS_KEY_ID !== !!config.S3_SECRET_ACCESS_KEY)
39
+ throw new Error("Set both S3 credentials or neither (IAM credential chain)");
40
+ if (config.NODE_ENV === "production" && config.DB_SYNCHRONIZE)
41
+ throw new Error("DB_SYNCHRONIZE is forbidden in production");
42
+ if (config.NODE_ENV === "production" && config.ADMIN_API_KEY.length < 32)
43
+ throw new Error("Production requires ADMIN_API_KEY of at least 32 characters until an authentication module is installed");
44
+ return config;
45
+ }
46
+ exports.config = readConfig();
47
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import "reflect-metadata";
2
+ import { DataSource, DataSourceOptions } from "typeorm";
3
+ import { ResourceDefinition } from "../resource/resource.types";
4
+ export declare function createCmsDataSource(resources: ResourceDefinition[], options?: Pick<DataSourceOptions, "migrations">): DataSource;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCmsDataSource = createCmsDataSource;
4
+ require("reflect-metadata");
5
+ const typeorm_1 = require("typeorm");
6
+ const config_1 = require("../config");
7
+ const resource_registry_1 = require("../resource/resource.registry");
8
+ const attachment_entity_1 = require("../storage/attachment.entity");
9
+ function createCmsDataSource(resources, options = {}) {
10
+ return new typeorm_1.DataSource({
11
+ type: config_1.config.DB_TYPE,
12
+ host: config_1.config.DB_HOST,
13
+ port: config_1.config.DB_PORT ?? (config_1.config.DB_TYPE === "mysql" ? 3306 : 5432),
14
+ username: config_1.config.DB_USERNAME,
15
+ password: config_1.config.DB_PASSWORD,
16
+ database: config_1.config.DB_DATABASE,
17
+ synchronize: config_1.config.DB_SYNCHRONIZE,
18
+ entities: [...resources.map(resource_registry_1.createEntity), attachment_entity_1.AttachmentEntity],
19
+ migrations: options.migrations ?? [],
20
+ ...(config_1.config.DB_TYPE === "mysql" ? { charset: "utf8mb4" } : {}),
21
+ });
22
+ }
23
+ //# sourceMappingURL=create-data-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-data-source.js","sourceRoot":"","sources":["../../../src/database/create-data-source.ts"],"names":[],"mappings":";;AAOA,kDAgBC;AAvBD,4BAA0B;AAC1B,qCAAwD;AACxD,sCAAmC;AAEnC,qEAA6D;AAC7D,oEAAgE;AAEhE,SAAgB,mBAAmB,CACjC,SAA+B,EAC/B,UAAiD,EAAE;IAEnD,OAAO,IAAI,oBAAU,CAAC;QACpB,IAAI,EAAE,eAAM,CAAC,OAAO;QACpB,IAAI,EAAE,eAAM,CAAC,OAAO;QACpB,IAAI,EAAE,eAAM,CAAC,OAAO,IAAI,CAAC,eAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAClE,QAAQ,EAAE,eAAM,CAAC,WAAW;QAC5B,QAAQ,EAAE,eAAM,CAAC,WAAW;QAC5B,QAAQ,EAAE,eAAM,CAAC,WAAW;QAC5B,WAAW,EAAE,eAAM,CAAC,cAAc;QAClC,QAAQ,EAAE,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,gCAAY,CAAC,EAAE,oCAAgB,CAAC;QAC5D,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;QACpC,GAAG,CAAC,eAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9D,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
2
+ import { Response } from "express";
3
+ export declare class ErrorFilter implements ExceptionFilter {
4
+ private readonly logger;
5
+ catch(error: unknown, host: ArgumentsHost): Response<any, Record<string, any>>;
6
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var ErrorFilter_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ErrorFilter = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const typeorm_1 = require("typeorm");
13
+ let ErrorFilter = ErrorFilter_1 = class ErrorFilter {
14
+ logger = new common_1.Logger(ErrorFilter_1.name);
15
+ catch(error, host) {
16
+ const response = host.switchToHttp().getResponse();
17
+ if (error instanceof common_1.HttpException)
18
+ return response.status(error.getStatus()).json(error.getResponse());
19
+ if (error instanceof typeorm_1.QueryFailedError) {
20
+ const code = String(error.driverError.code);
21
+ if ([
22
+ "23505",
23
+ "ER_DUP_ENTRY",
24
+ "23503",
25
+ "ER_ROW_IS_REFERENCED_2",
26
+ "ER_NO_REFERENCED_ROW_2",
27
+ ].includes(code))
28
+ return response.status(409).json({
29
+ statusCode: 409,
30
+ message: "Unique or reference constraint conflict",
31
+ });
32
+ if ([
33
+ "22P02",
34
+ "22003",
35
+ "22007",
36
+ "ER_TRUNCATED_WRONG_VALUE",
37
+ "ER_WRONG_VALUE",
38
+ "ER_DATA_OUT_OF_RANGE",
39
+ ].includes(code))
40
+ return response
41
+ .status(400)
42
+ .json({ statusCode: 400, message: "Invalid query value" });
43
+ }
44
+ this.logger.error(error instanceof Error ? error.message : "Unexpected error");
45
+ return response
46
+ .status(500)
47
+ .json({ statusCode: 500, message: "Internal server error" });
48
+ }
49
+ };
50
+ exports.ErrorFilter = ErrorFilter;
51
+ exports.ErrorFilter = ErrorFilter = ErrorFilter_1 = __decorate([
52
+ (0, common_1.Catch)()
53
+ ], ErrorFilter);
54
+ //# sourceMappingURL=error.filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.filter.js","sourceRoot":"","sources":["../../src/error.filter.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAMwB;AACxB,qCAA2C;AAGpC,IAAM,WAAW,mBAAjB,MAAM,WAAW;IACL,MAAM,GAAG,IAAI,eAAM,CAAC,aAAW,CAAC,IAAI,CAAC,CAAC;IACvD,KAAK,CAAC,KAAc,EAAE,IAAmB;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,WAAW,EAAY,CAAC;QAC7D,IAAI,KAAK,YAAY,sBAAa;YAChC,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QACtE,IAAI,KAAK,YAAY,0BAAgB,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAE,KAAK,CAAC,WAAiC,CAAC,IAAI,CAAC,CAAC;YACnE,IACE;gBACE,OAAO;gBACP,cAAc;gBACd,OAAO;gBACP,wBAAwB;gBACxB,wBAAwB;aACzB,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAEhB,OAAO,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC/B,UAAU,EAAE,GAAG;oBACf,OAAO,EAAE,yCAAyC;iBACnD,CAAC,CAAC;YACL,IACE;gBACE,OAAO;gBACP,OAAO;gBACP,OAAO;gBACP,0BAA0B;gBAC1B,gBAAgB;gBAChB,sBAAsB;aACvB,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAEhB,OAAO,QAAQ;qBACZ,MAAM,CAAC,GAAG,CAAC;qBACX,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAC5D,CAAC;QACF,OAAO,QAAQ;aACZ,MAAM,CAAC,GAAG,CAAC;aACX,IAAI,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;IACjE,CAAC;CACF,CAAA;AA1CY,kCAAW;sBAAX,WAAW;IADvB,IAAA,cAAK,GAAE;GACK,WAAW,CA0CvB"}
@@ -0,0 +1,12 @@
1
+ import "reflect-metadata";
2
+ export { z } from "zod";
3
+ export { ResourceModule } from "./resource/resource.module";
4
+ export type { ResourceModuleOptions } from "./resource/resource.module";
5
+ export { ResourceService } from "./resource/resource.service";
6
+ export { ResourceRegistry } from "./resource/resource.registry";
7
+ export { defineResource } from "./resource/resource.types";
8
+ export type { ResourceDefinition, ResourceHook, HookContext, HookAction, RecordData, AttachmentField, UploadedAttachment, } from "./resource/resource.types";
9
+ export { loadResources } from "./resource/resource.loader";
10
+ export { createCmsDataSource } from "./database/create-data-source";
11
+ export { AttachmentService } from "./storage/attachment.service";
12
+ export { configureCmsApp } from "./setup";
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureCmsApp = exports.AttachmentService = exports.createCmsDataSource = exports.loadResources = exports.defineResource = exports.ResourceRegistry = exports.ResourceService = exports.ResourceModule = exports.z = void 0;
4
+ require("reflect-metadata");
5
+ var zod_1 = require("zod");
6
+ Object.defineProperty(exports, "z", { enumerable: true, get: function () { return zod_1.z; } });
7
+ var resource_module_1 = require("./resource/resource.module");
8
+ Object.defineProperty(exports, "ResourceModule", { enumerable: true, get: function () { return resource_module_1.ResourceModule; } });
9
+ var resource_service_1 = require("./resource/resource.service");
10
+ Object.defineProperty(exports, "ResourceService", { enumerable: true, get: function () { return resource_service_1.ResourceService; } });
11
+ var resource_registry_1 = require("./resource/resource.registry");
12
+ Object.defineProperty(exports, "ResourceRegistry", { enumerable: true, get: function () { return resource_registry_1.ResourceRegistry; } });
13
+ var resource_types_1 = require("./resource/resource.types");
14
+ Object.defineProperty(exports, "defineResource", { enumerable: true, get: function () { return resource_types_1.defineResource; } });
15
+ var resource_loader_1 = require("./resource/resource.loader");
16
+ Object.defineProperty(exports, "loadResources", { enumerable: true, get: function () { return resource_loader_1.loadResources; } });
17
+ var create_data_source_1 = require("./database/create-data-source");
18
+ Object.defineProperty(exports, "createCmsDataSource", { enumerable: true, get: function () { return create_data_source_1.createCmsDataSource; } });
19
+ var attachment_service_1 = require("./storage/attachment.service");
20
+ Object.defineProperty(exports, "AttachmentService", { enumerable: true, get: function () { return attachment_service_1.AttachmentService; } });
21
+ var setup_1 = require("./setup");
22
+ Object.defineProperty(exports, "configureCmsApp", { enumerable: true, get: function () { return setup_1.configureCmsApp; } });
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,4BAA0B;AAC1B,2BAAwB;AAAf,wFAAA,CAAC,OAAA;AACV,8DAA4D;AAAnD,iHAAA,cAAc,OAAA;AAEvB,gEAA8D;AAArD,mHAAA,eAAe,OAAA;AACxB,kEAAgE;AAAvD,qHAAA,gBAAgB,OAAA;AACzB,4DAA2D;AAAlD,gHAAA,cAAc,OAAA;AAUvB,8DAA2D;AAAlD,gHAAA,aAAa,OAAA;AACtB,oEAAoE;AAA3D,yHAAA,mBAAmB,OAAA;AAC5B,mEAAiE;AAAxD,uHAAA,iBAAiB,OAAA;AAC1B,iCAA0C;AAAjC,wGAAA,eAAe,OAAA"}