@forge-cms/runtime 0.0.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -0
  3. package/dist/access.d.ts +30 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +64 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/config.d.ts +20 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +2 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/context.d.ts +16 -0
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +2 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/defaults.d.ts +20 -0
  16. package/dist/defaults.d.ts.map +1 -0
  17. package/dist/defaults.js +73 -0
  18. package/dist/defaults.js.map +1 -0
  19. package/dist/describe.d.ts +54 -0
  20. package/dist/describe.d.ts.map +1 -0
  21. package/dist/describe.js +88 -0
  22. package/dist/describe.js.map +1 -0
  23. package/dist/errors.d.ts +39 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +68 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/field-access.d.ts +14 -0
  28. package/dist/field-access.d.ts.map +1 -0
  29. package/dist/field-access.js +49 -0
  30. package/dist/field-access.js.map +1 -0
  31. package/dist/files.d.ts +24 -0
  32. package/dist/files.d.ts.map +1 -0
  33. package/dist/files.js +46 -0
  34. package/dist/files.js.map +1 -0
  35. package/dist/globals.d.ts +24 -0
  36. package/dist/globals.d.ts.map +1 -0
  37. package/dist/globals.js +161 -0
  38. package/dist/globals.js.map +1 -0
  39. package/dist/handlers.d.ts +37 -0
  40. package/dist/handlers.d.ts.map +1 -0
  41. package/dist/handlers.js +611 -0
  42. package/dist/handlers.js.map +1 -0
  43. package/dist/hooks.d.ts +60 -0
  44. package/dist/hooks.d.ts.map +1 -0
  45. package/dist/hooks.js +193 -0
  46. package/dist/hooks.js.map +1 -0
  47. package/dist/index.d.ts +18 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +26 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/localization.d.ts +52 -0
  52. package/dist/localization.d.ts.map +1 -0
  53. package/dist/localization.js +163 -0
  54. package/dist/localization.js.map +1 -0
  55. package/dist/operations.d.ts +70 -0
  56. package/dist/operations.d.ts.map +1 -0
  57. package/dist/operations.js +402 -0
  58. package/dist/operations.js.map +1 -0
  59. package/dist/populate.d.ts +6 -0
  60. package/dist/populate.d.ts.map +1 -0
  61. package/dist/populate.js +65 -0
  62. package/dist/populate.js.map +1 -0
  63. package/dist/relation-integrity.d.ts +39 -0
  64. package/dist/relation-integrity.d.ts.map +1 -0
  65. package/dist/relation-integrity.js +150 -0
  66. package/dist/relation-integrity.js.map +1 -0
  67. package/dist/runtime.d.ts +61 -0
  68. package/dist/runtime.d.ts.map +1 -0
  69. package/dist/runtime.js +155 -0
  70. package/dist/runtime.js.map +1 -0
  71. package/dist/versions.d.ts +51 -0
  72. package/dist/versions.d.ts.map +1 -0
  73. package/dist/versions.js +122 -0
  74. package/dist/versions.js.map +1 -0
  75. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ForgeCMS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @forge-cms/runtime
2
+
3
+ Runtime orchestrator for ForgeCMS. Binds schema definitions, database/auth/storage adapters, and HTTP handlers into a cohesive CMS runtime.
4
+
5
+ ## Responsibility
6
+
7
+ - Initialise and wire all adapters
8
+ - Sync database schema from collection definitions
9
+ - Provide CRUD request handlers that validate, authorise, and persist
10
+ - Expose a typed configuration API
11
+
12
+ ## Status
13
+
14
+ **Early design phase** — contracts and config types are stabilising. No real request handling is implemented yet.
@@ -0,0 +1,30 @@
1
+ import type { AccessArgs, AccessQuery, AccessRule, CmsUser, CollectionDefinition, FieldAccessArgs, FieldAccessRule } from '@forge-cms/core';
2
+ import type { DatabaseWhere } from '@forge-cms/db';
3
+ /**
4
+ * The outcome of an access check. `allowed: false` denies outright; `allowed: true` with a `where`
5
+ * grants access only to documents matching it (spec 020).
6
+ */
7
+ export interface AccessDecision {
8
+ allowed: boolean;
9
+ where?: DatabaseWhere;
10
+ }
11
+ /**
12
+ * Resolves a collection-level access rule.
13
+ *
14
+ * An `undefined` rule means "this rule is not configured" — the caller decides the fallback (the
15
+ * HTTP layer falls back to its route-level `allowedRoles`, the Local API treats it as public).
16
+ */
17
+ export declare function resolveAccess(rule: AccessRule | undefined, args: AccessArgs): Promise<AccessDecision | undefined>;
18
+ /** AND-merges an access constraint into a user-supplied query. Access conditions always win. */
19
+ export declare function mergeWhere(base: DatabaseWhere | undefined, constraint: DatabaseWhere | undefined): DatabaseWhere | undefined;
20
+ /**
21
+ * Checks an already-loaded document against an access constraint. Used for update/delete, where the
22
+ * constraint cannot be pushed into the query because the document is addressed by id.
23
+ */
24
+ export declare function documentMatches(doc: Record<string, unknown>, where: AccessQuery): boolean;
25
+ /** Resolves a field-level rule. `undefined` means unrestricted. */
26
+ export declare function resolveFieldAccess(rule: FieldAccessRule | undefined, args: FieldAccessArgs): Promise<boolean>;
27
+ /** The role string an access check will see for this user — exported for the HTTP layer's messages. */
28
+ export declare function userRoleOf(user: CmsUser | null): string | undefined;
29
+ export declare function accessArgs(collection: CollectionDefinition, args: Omit<AccessArgs, 'collection'>): AccessArgs;
30
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,UAAU,EACV,OAAO,EACP,oBAAoB,EACpB,eAAe,EACf,eAAe,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAkBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,UAAU,GAAG,SAAS,EAC5B,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAOrC;AAED,gGAAgG;AAChG,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,UAAU,EAAE,aAAa,GAAG,SAAS,GACpC,aAAa,GAAG,SAAS,CAI3B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAEzF;AAED,mEAAmE;AACnE,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAEnE;AAED,wBAAgB,UAAU,CACxB,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,GACnC,UAAU,CAEZ"}
package/dist/access.js ADDED
@@ -0,0 +1,64 @@
1
+ import { matchesCondition } from '@forge-cms/db';
2
+ const DENIED = { allowed: false };
3
+ const ALLOWED = { allowed: true };
4
+ function roleOf(user) {
5
+ if (!user)
6
+ return undefined;
7
+ if (typeof user.role === 'string')
8
+ return user.role;
9
+ return user.roles?.[0];
10
+ }
11
+ /** `string[]` rules keep their spec-010 meaning: the user's role must appear in the list. */
12
+ function decideByRoles(allowedRoles, user) {
13
+ const role = roleOf(user);
14
+ if (role === undefined)
15
+ return DENIED;
16
+ return allowedRoles.includes(role) ? ALLOWED : DENIED;
17
+ }
18
+ /**
19
+ * Resolves a collection-level access rule.
20
+ *
21
+ * An `undefined` rule means "this rule is not configured" — the caller decides the fallback (the
22
+ * HTTP layer falls back to its route-level `allowedRoles`, the Local API treats it as public).
23
+ */
24
+ export async function resolveAccess(rule, args) {
25
+ if (rule === undefined)
26
+ return undefined;
27
+ if (Array.isArray(rule))
28
+ return decideByRoles(rule, args.user);
29
+ const result = await rule(args);
30
+ if (typeof result === 'boolean')
31
+ return result ? ALLOWED : DENIED;
32
+ return { allowed: true, where: result };
33
+ }
34
+ /** AND-merges an access constraint into a user-supplied query. Access conditions always win. */
35
+ export function mergeWhere(base, constraint) {
36
+ if (!constraint || Object.keys(constraint).length === 0)
37
+ return base;
38
+ if (!base || Object.keys(base).length === 0)
39
+ return constraint;
40
+ return { ...base, ...constraint };
41
+ }
42
+ /**
43
+ * Checks an already-loaded document against an access constraint. Used for update/delete, where the
44
+ * constraint cannot be pushed into the query because the document is addressed by id.
45
+ */
46
+ export function documentMatches(doc, where) {
47
+ return Object.entries(where).every(([key, condition]) => matchesCondition(doc[key], condition));
48
+ }
49
+ /** Resolves a field-level rule. `undefined` means unrestricted. */
50
+ export async function resolveFieldAccess(rule, args) {
51
+ if (rule === undefined)
52
+ return true;
53
+ if (Array.isArray(rule))
54
+ return decideByRoles(rule, args.user).allowed;
55
+ return rule(args);
56
+ }
57
+ /** The role string an access check will see for this user — exported for the HTTP layer's messages. */
58
+ export function userRoleOf(user) {
59
+ return roleOf(user);
60
+ }
61
+ export function accessArgs(collection, args) {
62
+ return { ...args, collection };
63
+ }
64
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { CollectionDefinition, GlobalDefinition } from '@forge-cms/core';
2
+ import type { DatabaseAdapter } from '@forge-cms/db';
3
+ import type { AuthAdapter } from '@forge-cms/auth';
4
+ import type { StorageAdapter } from '@forge-cms/storage';
5
+ export interface AdapterSet {
6
+ database: DatabaseAdapter;
7
+ auth: AuthAdapter;
8
+ storage: StorageAdapter;
9
+ }
10
+ export interface ForgeCmsConfig<TEnv = unknown> {
11
+ /** Schema definitions for all collections */
12
+ collections: CollectionDefinition[];
13
+ /** Singleton documents (site-wide config: nav, footer, SEO defaults). */
14
+ globals?: GlobalDefinition[];
15
+ /** Adapter instances (must be initialised separately or via runtime.init()) */
16
+ adapters: AdapterSet;
17
+ /** Cloudflare/environment bindings passed to adapters */
18
+ env?: TEnv;
19
+ }
20
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,WAAW,cAAc,CAAC,IAAI,GAAG,OAAO;IAC5C,6CAA6C;IAC7C,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACpC,yEAAyE;IACzE,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,EAAE,UAAU,CAAC;IACrB,yDAAyD;IACzD,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ"}
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ import type { CollectionDefinition, GlobalDefinition } from '@forge-cms/core';
2
+ import type { AdapterSet } from './config.js';
3
+ /**
4
+ * The slice of `ForgeCmsRuntime` the operations need. Declared structurally rather than importing
5
+ * the class so `operations.ts` / `populate.ts` never import `runtime.ts` — `runtime.ts` imports
6
+ * *them*, and `import/no-cycle` is an error in this repo.
7
+ *
8
+ * `ForgeCmsRuntime` satisfies this interface structurally; no explicit `implements` is needed.
9
+ */
10
+ export interface OperationContext {
11
+ readonly adapters: AdapterSet;
12
+ getCollection(slug: string): CollectionDefinition | undefined;
13
+ getGlobal(slug: string): GlobalDefinition | undefined;
14
+ getCollections(): readonly CollectionDefinition[];
15
+ }
16
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAC;IAC9D,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACtD,cAAc,IAAI,SAAS,oBAAoB,EAAE,CAAC;CACnD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ import type { CollectionDefinition } from '@forge-cms/core';
2
+ /**
3
+ * The two field options that used to be declared and never read (spec 040, demo finding 1):
4
+ * `defaultValue` on any field, and `autoGenerate`/`sourceField` on a `slug`.
5
+ *
6
+ * Both run at the very start of a write, before `beforeValidate` hooks, so a hook can still override
7
+ * whatever they produced — and so validation sees the final value.
8
+ */
9
+ /** Fields whose incoming value is `undefined` take their `defaultValue`. Create only. */
10
+ export declare function applyFieldDefaults(collection: CollectionDefinition, data: Record<string, unknown>): Record<string, unknown>;
11
+ /**
12
+ * Fills `slug` fields marked `autoGenerate` from their source field, and normalises any slug the
13
+ * caller did provide.
14
+ *
15
+ * Runs on create and update. `existing` is the stored document on update, so renaming nothing but
16
+ * the title of a document whose slug was auto-generated does **not** silently break its URL — an
17
+ * existing non-empty slug is kept unless the caller explicitly clears it.
18
+ */
19
+ export declare function applyAutoSlugs(collection: CollectionDefinition, data: Record<string, unknown>, existing?: Record<string, unknown>): Record<string, unknown>;
20
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAoB,MAAM,iBAAiB,CAAC;AAE9E;;;;;;GAMG;AAEH,yFAAyF;AACzF,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB;AAKD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyCzB"}
@@ -0,0 +1,73 @@
1
+ import { slugify } from '@forge-cms/core';
2
+ /**
3
+ * The two field options that used to be declared and never read (spec 040, demo finding 1):
4
+ * `defaultValue` on any field, and `autoGenerate`/`sourceField` on a `slug`.
5
+ *
6
+ * Both run at the very start of a write, before `beforeValidate` hooks, so a hook can still override
7
+ * whatever they produced — and so validation sees the final value.
8
+ */
9
+ /** Fields whose incoming value is `undefined` take their `defaultValue`. Create only. */
10
+ export function applyFieldDefaults(collection, data) {
11
+ let result = data;
12
+ for (const [name, field] of Object.entries(collection.fields)) {
13
+ const defaultValue = field.options.defaultValue;
14
+ if (defaultValue === undefined || result[name] !== undefined)
15
+ continue;
16
+ // Copy lazily: most writes set every field they care about and need no new object at all.
17
+ if (result === data)
18
+ result = { ...data };
19
+ result[name] = defaultValue;
20
+ }
21
+ return result;
22
+ }
23
+ /** Candidate source fields when a `slug` declares `autoGenerate` without naming one. */
24
+ const IMPLICIT_SLUG_SOURCES = ['title', 'name'];
25
+ /**
26
+ * Fills `slug` fields marked `autoGenerate` from their source field, and normalises any slug the
27
+ * caller did provide.
28
+ *
29
+ * Runs on create and update. `existing` is the stored document on update, so renaming nothing but
30
+ * the title of a document whose slug was auto-generated does **not** silently break its URL — an
31
+ * existing non-empty slug is kept unless the caller explicitly clears it.
32
+ */
33
+ export function applyAutoSlugs(collection, data, existing) {
34
+ let result = data;
35
+ for (const [name, field] of Object.entries(collection.fields)) {
36
+ if (field.kind !== 'slug')
37
+ continue;
38
+ const provided = result[name];
39
+ const hasProvided = typeof provided === 'string' && provided.trim() !== '';
40
+ if (hasProvided) {
41
+ const normalised = slugify(provided);
42
+ if (normalised !== provided) {
43
+ if (result === data)
44
+ result = { ...data };
45
+ result[name] = normalised;
46
+ }
47
+ continue;
48
+ }
49
+ const options = field.options;
50
+ if (options.autoGenerate !== true)
51
+ continue;
52
+ // On update, an untouched field is absent from a partial body — leave the stored slug alone.
53
+ const clearedExplicitly = provided !== undefined;
54
+ const storedSlug = existing?.[name];
55
+ if (!clearedExplicitly && typeof storedSlug === 'string' && storedSlug !== '')
56
+ continue;
57
+ const merged = { ...existing, ...result };
58
+ const sourceNames = options.sourceField ? [options.sourceField] : IMPLICIT_SLUG_SOURCES;
59
+ const source = sourceNames
60
+ .map((sourceName) => merged[sourceName])
61
+ .find((value) => typeof value === 'string' && value.trim() !== '');
62
+ if (source === undefined)
63
+ continue;
64
+ const generated = slugify(source);
65
+ if (generated === '')
66
+ continue;
67
+ if (result === data)
68
+ result = { ...data };
69
+ result[name] = generated;
70
+ }
71
+ return result;
72
+ }
73
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { CollectionDefinition, FieldMap, GlobalDefinition } from '@forge-cms/core';
2
+ /** One selectable shape of a `blocks` field. Mirrors `BlockMeta` in `@forge-cms/angular`. */
3
+ export interface BlockDescription {
4
+ slug: string;
5
+ label: string;
6
+ fields: FieldDescription[];
7
+ }
8
+ /**
9
+ * The client-facing description of a field. Mirrors `FieldMeta` in `@forge-cms/angular` — the admin
10
+ * UI builds its form from this, so composite fields have to carry their nested structure or the form
11
+ * has no way to render them.
12
+ */
13
+ export interface FieldDescription {
14
+ name: string;
15
+ kind: string;
16
+ label: string;
17
+ required: boolean;
18
+ options?: string[];
19
+ relation?: {
20
+ collection: string;
21
+ many: boolean;
22
+ };
23
+ fields?: FieldDescription[];
24
+ blocks?: BlockDescription[];
25
+ minRows?: number;
26
+ maxRows?: number;
27
+ localized?: boolean;
28
+ }
29
+ export interface CollectionDescription {
30
+ slug: string;
31
+ name: string;
32
+ description: string;
33
+ fieldDefinitions: FieldDescription[];
34
+ /** `true` when the collection has draft/published status. The admin renders a status column. */
35
+ drafts?: boolean;
36
+ /** `true` when the collection accepts multipart uploads. The admin offers a file input. */
37
+ upload?: boolean;
38
+ /** Supported locales when the collection has localized fields. */
39
+ locales?: string[];
40
+ }
41
+ export interface GlobalDescription {
42
+ slug: string;
43
+ name: string;
44
+ description: string;
45
+ fieldDefinitions: FieldDescription[];
46
+ /** `true` when the global has draft/published status. */
47
+ drafts?: boolean;
48
+ }
49
+ export declare function describeFields(fields: FieldMap): FieldDescription[];
50
+ export declare function describeCollection(collection: CollectionDefinition): CollectionDescription;
51
+ export declare function describeCollections(collections: readonly CollectionDefinition[]): CollectionDescription[];
52
+ export declare function describeGlobal(global: GlobalDefinition): GlobalDescription;
53
+ export declare function describeGlobals(globals: readonly GlobalDefinition[]): GlobalDescription[];
54
+ //# sourceMappingURL=describe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe.d.ts","sourceRoot":"","sources":["../src/describe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,oBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAKjB,MAAM,iBAAiB,CAAC;AAEzB,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;IACjD,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;IACrC,gGAAgG;IAChG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;IACrC,yDAAyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAqED,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CAEnE;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,oBAAoB,GAAG,qBAAqB,CAW1F;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,oBAAoB,EAAE,GAC3C,qBAAqB,EAAE,CAEzB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAQ1E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,iBAAiB,EAAE,CAEzF"}
@@ -0,0 +1,88 @@
1
+ /** `durationMinutes` → `Duration minutes`, for fields that declare no explicit label. */
2
+ function humaniseFieldName(name) {
3
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ');
4
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
5
+ }
6
+ /** `service_categories` → `Service categories`. Slugs are what the admin shows as a title. */
7
+ function humanise(slug) {
8
+ const spaced = slug.replace(/[_-]+/g, ' ').trim();
9
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
10
+ }
11
+ function describeField(name, field) {
12
+ const base = {
13
+ name,
14
+ kind: field.kind,
15
+ label: field.options.label ?? humaniseFieldName(name),
16
+ required: field.options.required ?? false,
17
+ ...(field.options.localized === true && { localized: true })
18
+ };
19
+ switch (field.kind) {
20
+ case 'select':
21
+ return { ...base, options: field.options.options };
22
+ case 'relation': {
23
+ const options = field.options;
24
+ return { ...base, relation: { collection: options.collection, many: options.many ?? false } };
25
+ }
26
+ case 'upload': {
27
+ const options = field.options;
28
+ return { ...base, relation: { collection: options.collection, many: false } };
29
+ }
30
+ case 'group':
31
+ return { ...base, fields: describeFields(field.options.fields) };
32
+ case 'array': {
33
+ const options = field.options;
34
+ return {
35
+ ...base,
36
+ fields: describeFields(options.fields),
37
+ ...(options.minRows !== undefined && { minRows: options.minRows }),
38
+ ...(options.maxRows !== undefined && { maxRows: options.maxRows })
39
+ };
40
+ }
41
+ case 'blocks': {
42
+ const options = field.options;
43
+ return {
44
+ ...base,
45
+ blocks: options.blocks.map((block) => ({
46
+ slug: block.slug,
47
+ label: block.label ?? block.slug,
48
+ fields: describeFields(block.fields)
49
+ })),
50
+ ...(options.minRows !== undefined && { minRows: options.minRows }),
51
+ ...(options.maxRows !== undefined && { maxRows: options.maxRows })
52
+ };
53
+ }
54
+ default:
55
+ return base;
56
+ }
57
+ }
58
+ export function describeFields(fields) {
59
+ return Object.entries(fields).map(([name, field]) => describeField(name, field));
60
+ }
61
+ export function describeCollection(collection) {
62
+ return {
63
+ slug: collection.slug,
64
+ name: humanise(collection.slug),
65
+ description: `Content collection for ${collection.slug}`,
66
+ fieldDefinitions: describeFields(collection.fields),
67
+ ...(collection.drafts === true && { drafts: true }),
68
+ ...(collection.upload === true && { upload: true }),
69
+ ...(collection.locales !== undefined &&
70
+ collection.locales.length > 0 && { locales: collection.locales })
71
+ };
72
+ }
73
+ export function describeCollections(collections) {
74
+ return collections.map(describeCollection);
75
+ }
76
+ export function describeGlobal(global) {
77
+ return {
78
+ slug: global.slug,
79
+ name: humanise(global.slug),
80
+ description: `Singleton configuration for ${global.slug}`,
81
+ fieldDefinitions: describeFields(global.fields),
82
+ ...(global.drafts === true && { drafts: true })
83
+ };
84
+ }
85
+ export function describeGlobals(globals) {
86
+ return globals.map(describeGlobal);
87
+ }
88
+ //# sourceMappingURL=describe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe.js","sourceRoot":"","sources":["../src/describe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { ValidationError } from '@forge-cms/core';
2
+ export type ForgeErrorCode = 'NOT_FOUND' | 'INVALID_INPUT' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'UNKNOWN_FIELD' | 'INVALID_QUERY' | 'INTERNAL_ERROR';
3
+ export declare class ForgeError extends Error {
4
+ readonly status: number;
5
+ readonly code: ForgeErrorCode;
6
+ constructor(message: string, status: number, code: ForgeErrorCode);
7
+ }
8
+ export declare class NotFoundError extends ForgeError {
9
+ constructor(message: string);
10
+ }
11
+ export declare class InvalidInputError extends ForgeError {
12
+ constructor(message: string);
13
+ }
14
+ export declare class InvalidQueryError extends ForgeError {
15
+ constructor(message: string);
16
+ }
17
+ export declare class UnknownFieldError extends ForgeError {
18
+ constructor(message: string);
19
+ }
20
+ export declare class ValidationFailedError extends ForgeError {
21
+ readonly details: ValidationError[];
22
+ constructor(details: ValidationError[]);
23
+ }
24
+ export declare class UnauthorizedError extends ForgeError {
25
+ constructor(message?: string);
26
+ }
27
+ export declare class AccessDeniedError extends ForgeError {
28
+ constructor(message?: string);
29
+ }
30
+ export declare function isForgeError(err: unknown): err is ForgeError;
31
+ export interface ForgeApiErrorBody {
32
+ error: {
33
+ code: ForgeErrorCode;
34
+ message: string;
35
+ details?: unknown;
36
+ };
37
+ }
38
+ export declare function toApiErrorBody(err: ForgeError): ForgeApiErrorBody;
39
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,eAAe,GACf,kBAAkB,GAClB,cAAc,GACd,WAAW,GACX,eAAe,GACf,eAAe,GACf,gBAAgB,CAAC;AAErB,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc;CAMlE;AAED,qBAAa,aAAc,SAAQ,UAAU;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;gBAExB,OAAO,EAAE,eAAe,EAAE;CAIvC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAiB;CAGrC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAc;CAGlC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU,CAE5D;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE;QACL,IAAI,EAAE,cAAc,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,iBAAiB,CAgBjE"}
package/dist/errors.js ADDED
@@ -0,0 +1,68 @@
1
+ export class ForgeError extends Error {
2
+ status;
3
+ code;
4
+ constructor(message, status, code) {
5
+ super(message);
6
+ this.name = new.target.name;
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export class NotFoundError extends ForgeError {
12
+ constructor(message) {
13
+ super(message, 404, 'NOT_FOUND');
14
+ }
15
+ }
16
+ export class InvalidInputError extends ForgeError {
17
+ constructor(message) {
18
+ super(message, 400, 'INVALID_INPUT');
19
+ }
20
+ }
21
+ export class InvalidQueryError extends ForgeError {
22
+ constructor(message) {
23
+ super(message, 400, 'INVALID_QUERY');
24
+ }
25
+ }
26
+ export class UnknownFieldError extends ForgeError {
27
+ constructor(message) {
28
+ super(message, 400, 'UNKNOWN_FIELD');
29
+ }
30
+ }
31
+ export class ValidationFailedError extends ForgeError {
32
+ details;
33
+ constructor(details) {
34
+ super('Document validation failed', 400, 'VALIDATION_ERROR');
35
+ this.details = details;
36
+ }
37
+ }
38
+ export class UnauthorizedError extends ForgeError {
39
+ constructor(message = 'Unauthorized') {
40
+ super(message, 401, 'UNAUTHORIZED');
41
+ }
42
+ }
43
+ export class AccessDeniedError extends ForgeError {
44
+ constructor(message = 'Forbidden') {
45
+ super(message, 403, 'FORBIDDEN');
46
+ }
47
+ }
48
+ export function isForgeError(err) {
49
+ return err instanceof ForgeError;
50
+ }
51
+ export function toApiErrorBody(err) {
52
+ if (err instanceof ValidationFailedError) {
53
+ return {
54
+ error: {
55
+ code: err.code,
56
+ message: err.message,
57
+ details: err.details
58
+ }
59
+ };
60
+ }
61
+ return {
62
+ error: {
63
+ code: err.code,
64
+ message: err.message
65
+ }
66
+ };
67
+ }
68
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,MAAM,CAAS;IACf,IAAI,CAAiB;IAE9B,YAAY,OAAe,EAAE,MAAc,EAAE,IAAoB;QAC/D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,UAAU;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;IACnC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,UAAU;IAC1C,OAAO,CAAoB;IAEpC,YAAY,OAA0B;QACpC,KAAK,CAAC,4BAA4B,EAAE,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC;IACtC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;IACnC,CAAC;CACF;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,GAAG,YAAY,UAAU,CAAC;AACnC,CAAC;AAUD,MAAM,UAAU,cAAc,CAAC,GAAe;IAC5C,IAAI,GAAG,YAAY,qBAAqB,EAAE,CAAC;QACzC,OAAO;YACL,KAAK,EAAE;gBACL,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,OAAO,EAAE,GAAG,CAAC,OAAO;aACrB;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,OAAO,EAAE,GAAG,CAAC,OAAO;SACrB;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { CmsUser, CollectionDefinition } from '@forge-cms/core';
2
+ import type { DatabaseRecord } from '@forge-cms/db';
3
+ export declare class FieldAccessError extends Error {
4
+ readonly field: string;
5
+ constructor(field: string);
6
+ }
7
+ /**
8
+ * Omits fields the user may not read (`field.options.access.read`). A rule can now be a predicate as
9
+ * well as a role list (spec 020), so resolution is async.
10
+ */
11
+ export declare function filterReadableFields(record: DatabaseRecord, collection: CollectionDefinition, user: CmsUser | null): Promise<DatabaseRecord>;
12
+ /** Throws {@link FieldAccessError} naming the first field the user may not write. */
13
+ export declare function assertWritableFields(body: Record<string, unknown>, collection: CollectionDefinition, user: CmsUser | null, operation?: 'create' | 'update'): Promise<void>;
14
+ //# sourceMappingURL=field-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-access.d.ts","sourceRoot":"","sources":["../src/field-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,qBAAa,gBAAiB,SAAQ,KAAK;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAGnC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,qFAAqF;AACrF,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,OAAO,GAAG,IAAI,EACpB,SAAS,GAAE,QAAQ,GAAG,QAAmB,GACxC,OAAO,CAAC,IAAI,CAAC,CAcf"}