@contentrail/core 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -2
- package/dist/bounds.d.ts +36 -0
- package/dist/bounds.d.ts.map +1 -0
- package/dist/bounds.js +83 -0
- package/dist/bounds.js.map +1 -0
- package/dist/collection.d.ts +8 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +45 -0
- package/dist/collection.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +27 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +151 -0
- package/dist/validate.js.map +1 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ContentRail 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
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@contentrail/core`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-independent ContentRail schemas, validation, and inferred content types.
|
|
4
|
+
|
|
5
|
+
This package defines **collections and validation only**. It does not read files, talk to networks, store content, authenticate users, or publish sites.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js **≥ 22** (`engines.node`)
|
|
10
|
+
- TypeScript 5.9+ recommended for consumers
|
|
11
|
+
- [Zod](https://zod.dev) `^3.23` (dependency + peer `zod@^3.23.0`; shipped/tested on **3.25.76**)
|
|
12
|
+
|
|
13
|
+
Prefer `validateEntry` / `validateCollection` / `assertValidEntry` for untrusted
|
|
14
|
+
input. Calling `schema.safeParse()` directly bypasses ContentRail JSON bounds
|
|
15
|
+
(ADR 025).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
> **Not on the public npm registry yet.** Packages remain `"private": true` until
|
|
20
|
+
> publication is explicitly authorized. See `docs/PUBLISHING.md`.
|
|
21
|
+
|
|
22
|
+
### Monorepo (current)
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm install
|
|
26
|
+
pnpm --filter @contentrail/core build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Intended after first public `0.1.0` release
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add @contentrail/core zod
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Define a collection
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineCollection } from "@contentrail/core";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
|
|
41
|
+
const articles = defineCollection({
|
|
42
|
+
name: "articles",
|
|
43
|
+
schema: z.object({
|
|
44
|
+
title: z.string().min(1),
|
|
45
|
+
slug: z.string().min(1),
|
|
46
|
+
published: z.boolean(),
|
|
47
|
+
publishedAt: z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Collection names must match `^[a-z][a-z0-9_-]*$`.
|
|
53
|
+
|
|
54
|
+
### Unexpected fields
|
|
55
|
+
|
|
56
|
+
| `unknownKeys` | Behaviour |
|
|
57
|
+
| ------------------- | ----------------------------------------- |
|
|
58
|
+
| `"strip"` (default) | Unknown root-object keys are removed. |
|
|
59
|
+
| `"strict"` | Unknown root-object keys fail validation. |
|
|
60
|
+
|
|
61
|
+
The policy is applied when the schema is a Zod object. Non-object schemas are left unchanged.
|
|
62
|
+
|
|
63
|
+
## Validate an entry
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { validateEntry } from "@contentrail/core";
|
|
67
|
+
|
|
68
|
+
const result = validateEntry(articles, {
|
|
69
|
+
title: "Welcome",
|
|
70
|
+
slug: "welcome",
|
|
71
|
+
published: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
console.error(result.error.issues);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(result.data.title);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`validateEntry` never throws for invalid content. Use `assertValidEntry` when you prefer exceptions.
|
|
82
|
+
|
|
83
|
+
## Validate a collection (array of entries)
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { validateCollection } from "@contentrail/core";
|
|
87
|
+
|
|
88
|
+
const result = validateCollection(articles, [
|
|
89
|
+
{ title: "Welcome", slug: "welcome", published: true },
|
|
90
|
+
{ title: "Next", slug: "next", published: false },
|
|
91
|
+
]);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Failing issues are prefixed with the entry index, for example `1.title`.
|
|
95
|
+
|
|
96
|
+
## Type inference
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import type { InferEntry } from "@contentrail/core";
|
|
100
|
+
|
|
101
|
+
type Article = InferEntry<typeof articles>;
|
|
102
|
+
// { title: string; slug: string; published: boolean; publishedAt?: string }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
| Error | When |
|
|
108
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
109
|
+
| `ContentDefinitionError` | Invalid `defineCollection` input (thrown). |
|
|
110
|
+
| `ContentValidationError` | Invalid content (`validate*` result or `assertValidEntry`). |
|
|
111
|
+
|
|
112
|
+
`ContentValidationError` includes:
|
|
113
|
+
|
|
114
|
+
- `collection` — collection name
|
|
115
|
+
- `issues` — `{ path, code, message, expected?, received? }[]`
|
|
116
|
+
- `toJSON()` — stable serializable shape
|
|
117
|
+
|
|
118
|
+
## Public exports
|
|
119
|
+
|
|
120
|
+
From `@contentrail/core`:
|
|
121
|
+
|
|
122
|
+
- `defineCollection`
|
|
123
|
+
- `validateEntry`
|
|
124
|
+
- `validateCollection`
|
|
125
|
+
- `assertValidEntry`
|
|
126
|
+
- `ContentDefinitionError`
|
|
127
|
+
- `ContentValidationError`
|
|
128
|
+
- Types: `ContentCollection`, `DefineCollectionInput`, `InferEntry`, `UnknownKeysPolicy`, `ValidationResult`, `ValidationSuccess`, `ValidationFailure`, `ValidationIssue`
|
|
129
|
+
|
|
130
|
+
There is a single browser-safe export map entry (`.`). No server-only subpath exists in this package.
|
|
131
|
+
|
|
132
|
+
## Compatibility
|
|
133
|
+
|
|
134
|
+
- Runtime: any modern ESM environment (browsers, Node, Workers) that can run Zod
|
|
135
|
+
- Module format: ESM only
|
|
136
|
+
- Not coupled to Astro, Vite, Cloudflare, or React
|
|
137
|
+
|
|
138
|
+
## Current limitations
|
|
139
|
+
|
|
140
|
+
- No content I/O, Git, R2, or publishing APIs
|
|
141
|
+
- No editor schema introspection helpers beyond Zod itself
|
|
142
|
+
- No JSON Schema export
|
|
143
|
+
- Zod 4 is not targeted; peer range is Zod 3.x
|
|
144
|
+
- Unpublished while `"private": true` — see root README / `docs/PUBLISHING.md`
|
package/dist/bounds.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural limits applied **before** Zod validation.
|
|
3
|
+
*
|
|
4
|
+
* Zod 3 (and Zod 4's `zod/v3` entry) can accumulate one validation issue per
|
|
5
|
+
* array element without early termination (GHSA-mrhx-c7xc-m3w5). ContentRail
|
|
6
|
+
* stays on the Zod 3 public schema API; these bounds reduce resource-exhaustion
|
|
7
|
+
* risk at trust boundaries without migrating consumers to Zod 4.
|
|
8
|
+
*
|
|
9
|
+
* Limitations (explicit): these checks bound nesting, array length, and object
|
|
10
|
+
* key count. They do **not** cap string byte length, total serialized size, or
|
|
11
|
+
* CPU spent validating values that are within the structural limits.
|
|
12
|
+
*/
|
|
13
|
+
export interface JsonBoundLimits {
|
|
14
|
+
/** Maximum length of any array in the value tree. Default: 10_000. */
|
|
15
|
+
readonly maxArrayLength?: number;
|
|
16
|
+
/** Maximum number of own keys on any object. Default: 10_000. */
|
|
17
|
+
readonly maxObjectKeys?: number;
|
|
18
|
+
/** Maximum nesting depth (root = 0). Default: 32. */
|
|
19
|
+
readonly maxDepth?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface JsonBoundViolation {
|
|
22
|
+
readonly path: ReadonlyArray<string | number>;
|
|
23
|
+
readonly code: "too_big" | "too_deep" | "cyclic";
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Walks a JSON-like value and returns the first structural bound violation.
|
|
28
|
+
* Returns `null` when the value is within limits.
|
|
29
|
+
*
|
|
30
|
+
* Cyclic object graphs are rejected (`cyclic`) so the walker itself cannot be
|
|
31
|
+
* forced into unbounded recursion. Primitives (including large strings) are
|
|
32
|
+
* accepted by this helper — size must be constrained by callers (e.g. Worker
|
|
33
|
+
* body limits).
|
|
34
|
+
*/
|
|
35
|
+
export declare function findJsonBoundViolation(value: unknown, limits?: JsonBoundLimits): JsonBoundViolation | null;
|
|
36
|
+
//# sourceMappingURL=bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,iEAAiE;IACjE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAMD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,MAAM,GAAE,eAAoB,GAC3B,kBAAkB,GAAG,IAAI,CAc3B"}
|
package/dist/bounds.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural limits applied **before** Zod validation.
|
|
3
|
+
*
|
|
4
|
+
* Zod 3 (and Zod 4's `zod/v3` entry) can accumulate one validation issue per
|
|
5
|
+
* array element without early termination (GHSA-mrhx-c7xc-m3w5). ContentRail
|
|
6
|
+
* stays on the Zod 3 public schema API; these bounds reduce resource-exhaustion
|
|
7
|
+
* risk at trust boundaries without migrating consumers to Zod 4.
|
|
8
|
+
*
|
|
9
|
+
* Limitations (explicit): these checks bound nesting, array length, and object
|
|
10
|
+
* key count. They do **not** cap string byte length, total serialized size, or
|
|
11
|
+
* CPU spent validating values that are within the structural limits.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MAX_ARRAY_LENGTH = 10_000;
|
|
14
|
+
const DEFAULT_MAX_OBJECT_KEYS = 10_000;
|
|
15
|
+
const DEFAULT_MAX_DEPTH = 32;
|
|
16
|
+
/**
|
|
17
|
+
* Walks a JSON-like value and returns the first structural bound violation.
|
|
18
|
+
* Returns `null` when the value is within limits.
|
|
19
|
+
*
|
|
20
|
+
* Cyclic object graphs are rejected (`cyclic`) so the walker itself cannot be
|
|
21
|
+
* forced into unbounded recursion. Primitives (including large strings) are
|
|
22
|
+
* accepted by this helper — size must be constrained by callers (e.g. Worker
|
|
23
|
+
* body limits).
|
|
24
|
+
*/
|
|
25
|
+
export function findJsonBoundViolation(value, limits = {}) {
|
|
26
|
+
const maxArrayLength = limits.maxArrayLength ?? DEFAULT_MAX_ARRAY_LENGTH;
|
|
27
|
+
const maxObjectKeys = limits.maxObjectKeys ?? DEFAULT_MAX_OBJECT_KEYS;
|
|
28
|
+
const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
29
|
+
return walk(value, [], 0, maxArrayLength, maxObjectKeys, maxDepth, new WeakSet());
|
|
30
|
+
}
|
|
31
|
+
function walk(value, path, depth, maxArrayLength, maxObjectKeys, maxDepth, seen) {
|
|
32
|
+
if (depth > maxDepth) {
|
|
33
|
+
return {
|
|
34
|
+
path,
|
|
35
|
+
code: "too_deep",
|
|
36
|
+
message: `Value exceeds maximum nesting depth of ${maxDepth}.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (value === null || typeof value !== "object") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (seen.has(value)) {
|
|
43
|
+
return {
|
|
44
|
+
path,
|
|
45
|
+
code: "cyclic",
|
|
46
|
+
message: "Cyclic object graph is not allowed.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
seen.add(value);
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
if (value.length > maxArrayLength) {
|
|
52
|
+
return {
|
|
53
|
+
path,
|
|
54
|
+
code: "too_big",
|
|
55
|
+
message: `Array length ${value.length} exceeds limit ${maxArrayLength}.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
59
|
+
const nested = walk(value[index], [...path, index], depth + 1, maxArrayLength, maxObjectKeys, maxDepth, seen);
|
|
60
|
+
if (nested !== null) {
|
|
61
|
+
return nested;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const keys = Object.keys(value);
|
|
67
|
+
if (keys.length > maxObjectKeys) {
|
|
68
|
+
return {
|
|
69
|
+
path,
|
|
70
|
+
code: "too_big",
|
|
71
|
+
message: `Object key count ${keys.length} exceeds limit ${maxObjectKeys}.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const record = value;
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const nested = walk(record[key], [...path, key], depth + 1, maxArrayLength, maxObjectKeys, maxDepth, seen);
|
|
77
|
+
if (nested !== null) {
|
|
78
|
+
return nested;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.js","sourceRoot":"","sources":["../src/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AACvC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAAc,EACd,SAA0B,EAAE;IAE5B,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,wBAAwB,CAAC;IACzE,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IACtE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IAEtD,OAAO,IAAI,CACT,KAAK,EACL,EAAE,EACF,CAAC,EACD,cAAc,EACd,aAAa,EACb,QAAQ,EACR,IAAI,OAAO,EAAU,CACtB,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CACX,KAAc,EACd,IAAoC,EACpC,KAAa,EACb,cAAsB,EACtB,aAAqB,EACrB,QAAgB,EAChB,IAAqB;IAErB,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACrB,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,0CAA0C,QAAQ,GAAG;SAC/D,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,qCAAqC;SAC/C,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEhB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YAClC,OAAO;gBACL,IAAI;gBACJ,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,gBAAgB,KAAK,CAAC,MAAM,kBAAkB,cAAc,GAAG;aACzE,CAAC;QACJ,CAAC;QACD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,IAAI,CACjB,KAAK,CAAC,KAAK,CAAC,EACZ,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,EAChB,KAAK,GAAG,CAAC,EACT,cAAc,EACd,aAAa,EACb,QAAQ,EACR,IAAI,CACL,CAAC;YACF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;QAChC,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,kBAAkB,aAAa,GAAG;SAC3E,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,IAAI,CACjB,MAAM,CAAC,GAAG,CAAC,EACX,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,EACd,KAAK,GAAG,CAAC,EACT,cAAc,EACd,aAAa,EACb,QAAQ,EACR,IAAI,CACL,CAAC;QACF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type z } from "zod";
|
|
2
|
+
import type { ContentCollection, DefineCollectionInput } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Defines a content collection bound to a Zod schema.
|
|
5
|
+
* Does not touch storage or publishing — schema and validation only.
|
|
6
|
+
*/
|
|
7
|
+
export declare function defineCollection<TSchema extends z.ZodTypeAny>(input: DefineCollectionInput<TSchema>): ContentCollection<TSchema>;
|
|
8
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,CAAC,EAAE,MAAM,KAAK,CAAC;AAExC,OAAO,KAAK,EACV,iBAAiB,EACjB,qBAAqB,EAEtB,MAAM,YAAY,CAAC;AAIpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,CAAC,CAAC,UAAU,EAC3D,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,GACpC,iBAAiB,CAAC,OAAO,CAAC,CA4C5B"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ZodObject } from "zod";
|
|
2
|
+
import { ContentDefinitionError } from "./errors.js";
|
|
3
|
+
const COLLECTION_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
4
|
+
/**
|
|
5
|
+
* Defines a content collection bound to a Zod schema.
|
|
6
|
+
* Does not touch storage or publishing — schema and validation only.
|
|
7
|
+
*/
|
|
8
|
+
export function defineCollection(input) {
|
|
9
|
+
if (input === null || typeof input !== "object") {
|
|
10
|
+
throw new ContentDefinitionError("Collection definition must be a non-null object.");
|
|
11
|
+
}
|
|
12
|
+
const name = input.name;
|
|
13
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
14
|
+
throw new ContentDefinitionError('Collection "name" must be a non-empty string.');
|
|
15
|
+
}
|
|
16
|
+
if (!COLLECTION_NAME_PATTERN.test(name)) {
|
|
17
|
+
throw new ContentDefinitionError(`Collection "name" must match ${COLLECTION_NAME_PATTERN}: received "${name}".`);
|
|
18
|
+
}
|
|
19
|
+
if (input.schema === undefined || input.schema === null) {
|
|
20
|
+
throw new ContentDefinitionError(`Collection "${name}" requires a Zod "schema".`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof input.schema.safeParse !== "function") {
|
|
23
|
+
throw new ContentDefinitionError(`Collection "${name}" "schema" must be a Zod schema with safeParse().`);
|
|
24
|
+
}
|
|
25
|
+
const unknownKeys = input.unknownKeys ?? "strip";
|
|
26
|
+
if (unknownKeys !== "strip" && unknownKeys !== "strict") {
|
|
27
|
+
throw new ContentDefinitionError(`Collection "${name}" "unknownKeys" must be "strip" or "strict".`);
|
|
28
|
+
}
|
|
29
|
+
const schema = applyUnknownKeysPolicy(input.schema, unknownKeys);
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
name,
|
|
32
|
+
schema,
|
|
33
|
+
unknownKeys,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function applyUnknownKeysPolicy(schema, policy) {
|
|
37
|
+
if (!(schema instanceof ZodObject)) {
|
|
38
|
+
return schema;
|
|
39
|
+
}
|
|
40
|
+
if (policy === "strict") {
|
|
41
|
+
return schema.strict();
|
|
42
|
+
}
|
|
43
|
+
return schema.strip();
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAU,MAAM,KAAK,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAOrD,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAErD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAqC;IAErC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,IAAI,sBAAsB,CAC9B,kDAAkD,CACnD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,sBAAsB,CAC9B,+CAA+C,CAChD,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,sBAAsB,CAC9B,gCAAgC,uBAAuB,eAAe,IAAI,IAAI,CAC/E,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QACxD,MAAM,IAAI,sBAAsB,CAC9B,eAAe,IAAI,4BAA4B,CAChD,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QACjD,MAAM,IAAI,sBAAsB,CAC9B,eAAe,IAAI,mDAAmD,CACvE,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAsB,KAAK,CAAC,WAAW,IAAI,OAAO,CAAC;IACpE,IAAI,WAAW,KAAK,OAAO,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;QACxD,MAAM,IAAI,sBAAsB,CAC9B,eAAe,IAAI,8CAA8C,CAClE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,sBAAsB,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAY,CAAC;IAE5E,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI;QACJ,MAAM;QACN,WAAW;KACZ,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAC7B,MAAe,EACf,MAAyB;IAEzB,IAAI,CAAC,CAAC,MAAM,YAAY,SAAS,CAAC,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,OAAO,MAAM,CAAC,MAAM,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single structured validation issue.
|
|
3
|
+
* Paths use Zod-style segments (string keys and numeric array indexes).
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationIssue {
|
|
6
|
+
readonly path: ReadonlyArray<string | number>;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly expected?: string;
|
|
10
|
+
readonly received?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class ContentDefinitionError extends Error {
|
|
13
|
+
readonly code: "CONTENT_DEFINITION_ERROR";
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class ContentValidationError extends Error {
|
|
17
|
+
readonly code: "CONTENT_VALIDATION_ERROR";
|
|
18
|
+
readonly collection: string;
|
|
19
|
+
readonly issues: readonly ValidationIssue[];
|
|
20
|
+
constructor(collection: string, issues: readonly ValidationIssue[]);
|
|
21
|
+
toJSON(): {
|
|
22
|
+
name: string;
|
|
23
|
+
code: "CONTENT_VALIDATION_ERROR";
|
|
24
|
+
collection: string;
|
|
25
|
+
message: string;
|
|
26
|
+
issues: readonly ValidationIssue[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,0BAA0B,CAAU;gBAExC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,0BAA0B,CAAU;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;gBAEhC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,eAAe,EAAE;IAWlE,MAAM,IAAI;QACR,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,0BAA0B,CAAC;QACjC,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;KACpC;CASF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class ContentDefinitionError extends Error {
|
|
2
|
+
code = "CONTENT_DEFINITION_ERROR";
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ContentDefinitionError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ContentValidationError extends Error {
|
|
9
|
+
code = "CONTENT_VALIDATION_ERROR";
|
|
10
|
+
collection;
|
|
11
|
+
issues;
|
|
12
|
+
constructor(collection, issues) {
|
|
13
|
+
const summary = issues.length === 1
|
|
14
|
+
? formatIssue(issues[0])
|
|
15
|
+
: `${issues.length} validation issues`;
|
|
16
|
+
super(`Invalid content for collection "${collection}": ${summary}`);
|
|
17
|
+
this.name = "ContentValidationError";
|
|
18
|
+
this.collection = collection;
|
|
19
|
+
this.issues = issues;
|
|
20
|
+
}
|
|
21
|
+
toJSON() {
|
|
22
|
+
return {
|
|
23
|
+
name: this.name,
|
|
24
|
+
code: this.code,
|
|
25
|
+
collection: this.collection,
|
|
26
|
+
message: this.message,
|
|
27
|
+
issues: this.issues,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function formatIssue(issue) {
|
|
32
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
33
|
+
return `${path}: ${issue.message}`;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,0BAAmC,CAAC;IAEpD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,0BAAmC,CAAC;IAC3C,UAAU,CAAS;IACnB,MAAM,CAA6B;IAE5C,YAAY,UAAkB,EAAE,MAAkC;QAChE,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,KAAK,CAAC;YACjB,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;YACzB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,oBAAoB,CAAC;QAC3C,KAAK,CAAC,mCAAmC,UAAU,MAAM,OAAO,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,MAAM;QAOJ,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC;IACJ,CAAC;CACF;AAED,SAAS,WAAW,CAAC,KAAsB;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACrE,OAAO,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;AACrC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { findJsonBoundViolation, type JsonBoundLimits, type JsonBoundViolation, } from "./bounds.js";
|
|
2
|
+
export { defineCollection } from "./collection.js";
|
|
3
|
+
export { ContentDefinitionError, ContentValidationError, type ValidationIssue, } from "./errors.js";
|
|
4
|
+
export type { ContentCollection, DefineCollectionInput, InferEntry, UnknownKeysPolicy, ValidationFailure, ValidationResult, ValidationSuccess, } from "./types.js";
|
|
5
|
+
export { DEFAULT_MAX_COLLECTION_ENTRIES, assertValidEntry, validateCollection, validateEntry, type ValidateCollectionOptions, type ValidateEntryOptions, } from "./validate.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,8BAA8B,EAC9B,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findJsonBoundViolation, } from "./bounds.js";
|
|
2
|
+
export { defineCollection } from "./collection.js";
|
|
3
|
+
export { ContentDefinitionError, ContentValidationError, } from "./errors.js";
|
|
4
|
+
export { DEFAULT_MAX_COLLECTION_ENTRIES, assertValidEntry, validateCollection, validateEntry, } from "./validate.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,GAGvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GAEvB,MAAM,aAAa,CAAC;AAUrB,OAAO,EACL,8BAA8B,EAC9B,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,GAGd,MAAM,eAAe,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ContentValidationError } from "./errors.js";
|
|
3
|
+
/** How root object schemas treat keys not declared in the schema. */
|
|
4
|
+
export type UnknownKeysPolicy = "strip" | "strict";
|
|
5
|
+
export interface DefineCollectionInput<TSchema extends z.ZodTypeAny> {
|
|
6
|
+
/** Stable collection identifier used in errors and tooling. */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Zod schema describing one content entry. */
|
|
9
|
+
readonly schema: TSchema;
|
|
10
|
+
/**
|
|
11
|
+
* Unexpected root-object field policy.
|
|
12
|
+
* - `strip` (default): remove unknown keys (Zod object default).
|
|
13
|
+
* - `strict`: reject unknown keys.
|
|
14
|
+
*
|
|
15
|
+
* Applies only when `schema` is a Zod object schema (including `.strict()` /
|
|
16
|
+
* `.passthrough()` wrappers replaced according to this policy).
|
|
17
|
+
*/
|
|
18
|
+
readonly unknownKeys?: UnknownKeysPolicy;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A defined content collection. This is a schema-bound definition only —
|
|
22
|
+
* it does not read, write, or publish content.
|
|
23
|
+
*/
|
|
24
|
+
export interface ContentCollection<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly schema: TSchema;
|
|
27
|
+
readonly unknownKeys: UnknownKeysPolicy;
|
|
28
|
+
}
|
|
29
|
+
/** Inferred entry type for a collection. */
|
|
30
|
+
export type InferEntry<TCollection extends ContentCollection> = z.infer<TCollection["schema"]>;
|
|
31
|
+
export type ValidationSuccess<T> = {
|
|
32
|
+
readonly success: true;
|
|
33
|
+
readonly data: T;
|
|
34
|
+
};
|
|
35
|
+
export type ValidationFailure = {
|
|
36
|
+
readonly success: false;
|
|
37
|
+
readonly error: ContentValidationError;
|
|
38
|
+
};
|
|
39
|
+
export type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
|
|
40
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEnD,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,UAAU;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;CAC1C;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAChC,OAAO,SAAS,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU;IAE3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;CACzC;AAED,4CAA4C;AAC5C,MAAM,MAAM,UAAU,CAAC,WAAW,SAAS,iBAAiB,IAAI,CAAC,CAAC,KAAK,CACrE,WAAW,CAAC,QAAQ,CAAC,CACtB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI;IACjC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type JsonBoundLimits } from "./bounds.js";
|
|
2
|
+
import type { ContentCollection, InferEntry, ValidationResult } from "./types.js";
|
|
3
|
+
/** Default cap for `validateCollection` input length (entries, not nested fields). */
|
|
4
|
+
export declare const DEFAULT_MAX_COLLECTION_ENTRIES = 10000;
|
|
5
|
+
export interface ValidateEntryOptions {
|
|
6
|
+
readonly bounds?: JsonBoundLimits;
|
|
7
|
+
}
|
|
8
|
+
export interface ValidateCollectionOptions extends ValidateEntryOptions {
|
|
9
|
+
/** Maximum number of top-level entries. Default: 10_000. */
|
|
10
|
+
readonly maxEntries?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validates a single content entry against the collection schema.
|
|
14
|
+
* Never throws for invalid content — returns a structured result.
|
|
15
|
+
*
|
|
16
|
+
* Structural JSON bounds run before Zod so oversized nested arrays/objects
|
|
17
|
+
* cannot drive unbounded issue accumulation in Zod 3 (GHSA-mrhx-c7xc-m3w5).
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateEntry<TCollection extends ContentCollection>(collection: TCollection, entry: unknown, options?: ValidateEntryOptions): ValidationResult<InferEntry<TCollection>>;
|
|
20
|
+
/**
|
|
21
|
+
* Validates an ordered list of entries for a collection.
|
|
22
|
+
* Aggregates issues from every failing entry; paths are prefixed with the index.
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateCollection<TCollection extends ContentCollection>(collection: TCollection, entries: unknown, options?: ValidateCollectionOptions): ValidationResult<Array<InferEntry<TCollection>>>;
|
|
25
|
+
/** Narrow helper for callers that prefer throwing on failure. */
|
|
26
|
+
export declare function assertValidEntry<TCollection extends ContentCollection>(collection: TCollection, entry: unknown, options?: ValidateEntryOptions): InferEntry<TCollection>;
|
|
27
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,WAAW,yBAA0B,SAAQ,oBAAoB;IACrE,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,WAAW,SAAS,iBAAiB,EACjE,UAAU,EAAE,WAAW,EACvB,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,oBAAyB,GACjC,gBAAgB,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAgC3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,SAAS,iBAAiB,EACtE,UAAU,EAAE,WAAW,EACvB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,yBAA8B,GACtC,gBAAgB,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CA+DlD;AA0DD,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,WAAW,SAAS,iBAAiB,EACpE,UAAU,EAAE,WAAW,EACvB,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,oBAAyB,GACjC,UAAU,CAAC,WAAW,CAAC,CAMzB"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { findJsonBoundViolation } from "./bounds.js";
|
|
2
|
+
import { ContentValidationError } from "./errors.js";
|
|
3
|
+
/** Default cap for `validateCollection` input length (entries, not nested fields). */
|
|
4
|
+
export const DEFAULT_MAX_COLLECTION_ENTRIES = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* Validates a single content entry against the collection schema.
|
|
7
|
+
* Never throws for invalid content — returns a structured result.
|
|
8
|
+
*
|
|
9
|
+
* Structural JSON bounds run before Zod so oversized nested arrays/objects
|
|
10
|
+
* cannot drive unbounded issue accumulation in Zod 3 (GHSA-mrhx-c7xc-m3w5).
|
|
11
|
+
*/
|
|
12
|
+
export function validateEntry(collection, entry, options = {}) {
|
|
13
|
+
assertCollection(collection);
|
|
14
|
+
const bound = findJsonBoundViolation(entry, options.bounds);
|
|
15
|
+
if (bound !== null) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: new ContentValidationError(collection.name, [
|
|
19
|
+
{
|
|
20
|
+
path: [...bound.path],
|
|
21
|
+
code: bound.code,
|
|
22
|
+
message: bound.message,
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const parsed = collection.schema.safeParse(entry);
|
|
28
|
+
if (parsed.success) {
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
data: parsed.data,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: new ContentValidationError(collection.name, parsed.error.issues.map(toValidationIssue)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validates an ordered list of entries for a collection.
|
|
41
|
+
* Aggregates issues from every failing entry; paths are prefixed with the index.
|
|
42
|
+
*/
|
|
43
|
+
export function validateCollection(collection, entries, options = {}) {
|
|
44
|
+
assertCollection(collection);
|
|
45
|
+
if (!Array.isArray(entries)) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: new ContentValidationError(collection.name, [
|
|
49
|
+
{
|
|
50
|
+
path: [],
|
|
51
|
+
code: "invalid_type",
|
|
52
|
+
message: "Collection entries must be an array.",
|
|
53
|
+
expected: "array",
|
|
54
|
+
received: typeName(entries),
|
|
55
|
+
},
|
|
56
|
+
]),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_COLLECTION_ENTRIES;
|
|
60
|
+
if (entries.length > maxEntries) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: new ContentValidationError(collection.name, [
|
|
64
|
+
{
|
|
65
|
+
path: [],
|
|
66
|
+
code: "too_big",
|
|
67
|
+
message: `Collection has ${entries.length} entries; limit is ${maxEntries}.`,
|
|
68
|
+
},
|
|
69
|
+
]),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const data = [];
|
|
73
|
+
const issues = [];
|
|
74
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
75
|
+
const entryOptions = options.bounds === undefined ? {} : { bounds: options.bounds };
|
|
76
|
+
const result = validateEntry(collection, entries[index], entryOptions);
|
|
77
|
+
if (result.success) {
|
|
78
|
+
data.push(result.data);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const issue of result.error.issues) {
|
|
82
|
+
issues.push({
|
|
83
|
+
path: [index, ...issue.path],
|
|
84
|
+
code: issue.code,
|
|
85
|
+
message: issue.message,
|
|
86
|
+
...(issue.expected !== undefined ? { expected: issue.expected } : {}),
|
|
87
|
+
...(issue.received !== undefined ? { received: issue.received } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (issues.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: new ContentValidationError(collection.name, issues),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { success: true, data };
|
|
98
|
+
}
|
|
99
|
+
function assertCollection(collection) {
|
|
100
|
+
if (collection === null ||
|
|
101
|
+
typeof collection !== "object" ||
|
|
102
|
+
typeof collection.name !== "string" ||
|
|
103
|
+
collection.schema === undefined ||
|
|
104
|
+
typeof collection.schema.safeParse !== "function") {
|
|
105
|
+
throw new TypeError("validateEntry/validateCollection require a ContentCollection from defineCollection().");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function toValidationIssue(issue) {
|
|
109
|
+
const base = {
|
|
110
|
+
path: issue.path.map((segment) => {
|
|
111
|
+
if (typeof segment === "string" || typeof segment === "number") {
|
|
112
|
+
return segment;
|
|
113
|
+
}
|
|
114
|
+
return String(segment);
|
|
115
|
+
}),
|
|
116
|
+
code: issue.code,
|
|
117
|
+
message: issue.message,
|
|
118
|
+
};
|
|
119
|
+
if (issue.code === "invalid_type") {
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
expected: issue.expected,
|
|
123
|
+
received: issue.received,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (issue.code === "unrecognized_keys") {
|
|
127
|
+
return {
|
|
128
|
+
...base,
|
|
129
|
+
received: issue.keys.join(", "),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return base;
|
|
133
|
+
}
|
|
134
|
+
function typeName(value) {
|
|
135
|
+
if (value === null) {
|
|
136
|
+
return "null";
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
return "array";
|
|
140
|
+
}
|
|
141
|
+
return typeof value;
|
|
142
|
+
}
|
|
143
|
+
/** Narrow helper for callers that prefer throwing on failure. */
|
|
144
|
+
export function assertValidEntry(collection, entry, options = {}) {
|
|
145
|
+
const result = validateEntry(collection, entry, options);
|
|
146
|
+
if (!result.success) {
|
|
147
|
+
throw result.error;
|
|
148
|
+
}
|
|
149
|
+
return result.data;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrail/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Framework-independent ContentRail schemas, validation, and content types.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"zod": "^3.25.76"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"zod": "^3.23.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.20.5",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"zod": "3.25.76"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Hridhin-k/contentrail.git",
|
|
38
|
+
"directory": "packages/core"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Hridhin-k/contentrail/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Hridhin-k/contentrail#readme",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.type-tests.json",
|
|
50
|
+
"test": "pnpm run build && tsc -p tsconfig.test.json && node --test test-dist/**/*.test.js"
|
|
51
|
+
}
|
|
6
52
|
}
|