@leodamours/jsonapi-dsl 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Léo Damours
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,149 @@
1
+ # @leodamours/jsonapi-dsl
2
+
3
+ [![CI](https://github.com/LeoDamours/json-api-client/actions/workflows/ci.yml/badge.svg)](https://github.com/LeoDamours/json-api-client/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@leodamours/jsonapi-dsl)](https://www.npmjs.com/package/@leodamours/jsonapi-dsl)
5
+
6
+ A TypeScript-first DSL for [JSON:API](https://jsonapi.org/) — define resources once, infer every type.
7
+
8
+ Zero dependencies. Zero runtime overhead beyond tiny marker objects.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @leodamours/jsonapi-dsl
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```ts
19
+ import { defineResource, t, hasOne, hasMany } from "@leodamours/jsonapi-dsl";
20
+ import type { InferResource, JsonApiDocument } from "@leodamours/jsonapi-dsl";
21
+
22
+ const User = defineResource("users", {
23
+ name: t.string(),
24
+ email: t.string(),
25
+ age: t.nullable(t.number()),
26
+ });
27
+
28
+ const Post = defineResource(
29
+ "posts",
30
+ {
31
+ title: t.string(),
32
+ body: t.string(),
33
+ publishedAt: t.nullable(t.date()),
34
+ tags: t.array(t.string()),
35
+ },
36
+ {
37
+ author: hasOne("users"),
38
+ comments: hasMany("comments"),
39
+ }
40
+ );
41
+
42
+ // Full resource type inferred from definition
43
+ type PostResource = InferResource<typeof Post>;
44
+
45
+ // Use in API response typing
46
+ type PostDocument = JsonApiDocument<PostResource>;
47
+ ```
48
+
49
+ ## Type Builders
50
+
51
+ | Builder | TypeScript type | Example |
52
+ |---------|----------------|---------|
53
+ | `t.string()` | `string` | `name: t.string()` |
54
+ | `t.number()` | `number` | `age: t.number()` |
55
+ | `t.boolean()` | `boolean` | `active: t.boolean()` |
56
+ | `t.date()` | `Date` | `createdAt: t.date()` |
57
+ | `t.nullable(inner)` | `T \| null` | `bio: t.nullable(t.string())` |
58
+ | `t.array(inner)` | `T[]` | `tags: t.array(t.string())` |
59
+ | `t.optional(inner)` | `T \| undefined` | `nickname: t.optional(t.string())` |
60
+
61
+ Builders compose — `t.nullable(t.array(t.string()))` infers as `string[] | null`.
62
+
63
+ ## Relationships
64
+
65
+ ```ts
66
+ import { hasOne, hasMany } from "@leodamours/jsonapi-dsl";
67
+
68
+ const Article = defineResource(
69
+ "articles",
70
+ { title: t.string() },
71
+ {
72
+ author: hasOne("users"), // { data: { type: "users"; id: string } | null }
73
+ comments: hasMany("comments"), // { data: { type: "comments"; id: string }[] }
74
+ }
75
+ );
76
+ ```
77
+
78
+ Relationship types enforce literal type strings — `hasOne("users")` won't accept `{ type: "people" }`.
79
+
80
+ ## Per-Verb Attribute Narrowing
81
+
82
+ Control which attributes are writable for create vs update:
83
+
84
+ ```ts
85
+ const Author = defineResource(
86
+ "authors",
87
+ {
88
+ name: t.string(),
89
+ email: t.string(),
90
+ createdAt: t.string(), // read-only, set by server
91
+ },
92
+ {
93
+ posts: hasMany("posts"),
94
+ },
95
+ {
96
+ create: ["name", "email"] as const, // createdAt excluded from create
97
+ update: ["name"] as const, // only name editable on update
98
+ }
99
+ );
100
+ ```
101
+
102
+ When no options are provided, all attributes are writable (backward compatible).
103
+
104
+ This narrowing is consumed by `@leodamours/jsonapi-client`'s `CreatePayload` and `UpdatePayload` types.
105
+
106
+ ## Inference Utilities
107
+
108
+ ```ts
109
+ import type {
110
+ InferResource,
111
+ InferResourceAttributes,
112
+ InferResourceRelationships,
113
+ InferResourceType,
114
+ InferType,
115
+ InferAttributes,
116
+ } from "@leodamours/jsonapi-dsl";
117
+
118
+ type PostResource = InferResource<typeof Post>;
119
+ // { type: "posts"; id?: string; attributes: { title: string; ... }; relationships: { ... } }
120
+
121
+ type PostAttrs = InferResourceAttributes<typeof Post>;
122
+ // { title: string; body: string; publishedAt: Date | null; tags: string[] }
123
+
124
+ type PostRels = InferResourceRelationships<typeof Post>;
125
+ // { author: JsonApiRelationship & { data: { type: "users"; id: string } | null }; ... }
126
+
127
+ type PostType = InferResourceType<typeof Post>;
128
+ // "posts"
129
+ ```
130
+
131
+ ## JSON:API Core Types
132
+
133
+ The package also exports all standard JSON:API types:
134
+
135
+ ```ts
136
+ import type {
137
+ JsonApiResource,
138
+ JsonApiDocument,
139
+ JsonApiCollectionDocument,
140
+ JsonApiErrorDocument,
141
+ JsonApiRelationship,
142
+ JsonApiLinks,
143
+ JsonApiMeta,
144
+ } from "@leodamours/jsonapi-dsl";
145
+ ```
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Core JSON:API specification types
3
+ * @see https://jsonapi.org/format/
4
+ */
5
+ interface JsonApiResourceIdentifier {
6
+ type: string;
7
+ id: string;
8
+ }
9
+ interface JsonApiLinks {
10
+ self?: string;
11
+ related?: string;
12
+ [key: string]: string | undefined;
13
+ }
14
+ type JsonApiMeta = Record<string, unknown>;
15
+ interface JsonApiRelationship {
16
+ data?: JsonApiResourceIdentifier | JsonApiResourceIdentifier[] | null;
17
+ links?: JsonApiLinks;
18
+ meta?: JsonApiMeta;
19
+ }
20
+ interface JsonApiRelationships {
21
+ [key: string]: JsonApiRelationship;
22
+ }
23
+ interface JsonApiResource<Attr extends Record<string, unknown> = Record<string, unknown>, Rel extends JsonApiRelationships = JsonApiRelationships> {
24
+ type: string;
25
+ id?: string;
26
+ attributes?: Attr;
27
+ relationships?: Rel;
28
+ links?: JsonApiLinks;
29
+ meta?: JsonApiMeta;
30
+ }
31
+ interface JsonApiError {
32
+ id?: string;
33
+ status?: string;
34
+ code?: string;
35
+ title?: string;
36
+ detail?: string;
37
+ source?: {
38
+ pointer?: string;
39
+ parameter?: string;
40
+ header?: string;
41
+ };
42
+ meta?: JsonApiMeta;
43
+ }
44
+ interface JsonApiDocumentLinks extends JsonApiLinks {
45
+ first?: string;
46
+ last?: string;
47
+ prev?: string;
48
+ next?: string;
49
+ }
50
+ interface JsonApiDocumentBase {
51
+ links?: JsonApiDocumentLinks;
52
+ meta?: JsonApiMeta;
53
+ jsonapi?: {
54
+ version?: string;
55
+ meta?: JsonApiMeta;
56
+ };
57
+ }
58
+
59
+ interface JsonApiDocument<T = JsonApiResource> extends JsonApiDocumentBase {
60
+ data: T;
61
+ included?: JsonApiResource[];
62
+ }
63
+ interface JsonApiCollectionDocument<T = JsonApiResource> extends JsonApiDocumentBase {
64
+ data: T[];
65
+ included?: JsonApiResource[];
66
+ }
67
+ interface JsonApiErrorDocument extends JsonApiDocumentBase {
68
+ errors: JsonApiError[];
69
+ }
70
+ interface JsonApiEmptyDocument extends JsonApiDocumentBase {
71
+ data: null;
72
+ }
73
+ type JsonApiResponseDocument<T = JsonApiResource> = JsonApiDocument<T> | JsonApiCollectionDocument<T> | JsonApiErrorDocument | JsonApiEmptyDocument;
74
+
75
+ /**
76
+ * Type builder markers and the `t` namespace
77
+ *
78
+ * Each builder is a lightweight runtime object whose TypeScript type
79
+ * drives compile-time inference via InferType<T>.
80
+ */
81
+ interface StringType {
82
+ readonly _type: "string";
83
+ }
84
+ interface NumberType {
85
+ readonly _type: "number";
86
+ }
87
+ interface BooleanType {
88
+ readonly _type: "boolean";
89
+ }
90
+ interface DateType {
91
+ readonly _type: "date";
92
+ }
93
+ interface NullableType<Inner extends TypeBuilder> {
94
+ readonly _type: "nullable";
95
+ readonly inner: Inner;
96
+ }
97
+ interface ArrayType<Inner extends TypeBuilder> {
98
+ readonly _type: "array";
99
+ readonly inner: Inner;
100
+ }
101
+ interface OptionalType<Inner extends TypeBuilder> {
102
+ readonly _type: "optional";
103
+ readonly inner: Inner;
104
+ }
105
+ type TypeBuilder = StringType | NumberType | BooleanType | DateType | NullableType<any> | ArrayType<any> | OptionalType<any>;
106
+ type AttributesSchema = Record<string, TypeBuilder>;
107
+ declare const t: {
108
+ string: () => StringType;
109
+ number: () => NumberType;
110
+ boolean: () => BooleanType;
111
+ date: () => DateType;
112
+ nullable: <T extends TypeBuilder>(inner: T) => NullableType<T>;
113
+ array: <T extends TypeBuilder>(inner: T) => ArrayType<T>;
114
+ optional: <T extends TypeBuilder>(inner: T) => OptionalType<T>;
115
+ };
116
+
117
+ /**
118
+ * Relationship definition helpers
119
+ */
120
+ interface HasOneRelationship<T extends string = string> {
121
+ readonly _rel: "one";
122
+ readonly type: T;
123
+ }
124
+ interface HasManyRelationship<T extends string = string> {
125
+ readonly _rel: "many";
126
+ readonly type: T;
127
+ }
128
+ type RelationshipBuilder = HasOneRelationship | HasManyRelationship;
129
+ type RelationshipsSchema = Record<string, RelationshipBuilder>;
130
+ declare const hasOne: <T extends string>(type: T) => HasOneRelationship<T>;
131
+ declare const hasMany: <T extends string>(type: T) => HasManyRelationship<T>;
132
+
133
+ /**
134
+ * A resource definition that captures the type-level information
135
+ * needed for inference.
136
+ */
137
+ interface ResourceDefinition<Type extends string = string, Attrs extends AttributesSchema = AttributesSchema, Rels extends RelationshipsSchema = RelationshipsSchema> {
138
+ readonly resourceType: Type;
139
+ readonly attributes: Attrs;
140
+ readonly relationships: Rels;
141
+ readonly createKeys?: readonly string[];
142
+ readonly updateKeys?: readonly string[];
143
+ }
144
+ /**
145
+ * Options for per-verb attribute narrowing.
146
+ * `create` lists which attribute keys are accepted by POST.
147
+ * `update` lists which attribute keys are accepted by PATCH.
148
+ */
149
+ interface ResourceOptions<CK extends string = string, UK extends string = string> {
150
+ readonly create: readonly CK[];
151
+ readonly update: readonly UK[];
152
+ }
153
+ /** No relationships. */
154
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema>(type: Type, attributes: Attrs): ResourceDefinition<Type, Attrs, Record<string, never>>;
155
+ /** With relationships. */
156
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema, Rels extends RelationshipsSchema>(type: Type, attributes: Attrs, relationships: Rels): ResourceDefinition<Type, Attrs, Rels>;
157
+ /** With relationships + per-verb attribute options. */
158
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema, Rels extends RelationshipsSchema, CK extends keyof Attrs & string, UK extends keyof Attrs & string>(type: Type, attributes: Attrs, relationships: Rels, options: ResourceOptions<CK, UK>): ResourceDefinition<Type, Attrs, Rels> & {
159
+ readonly createKeys: readonly CK[];
160
+ readonly updateKeys: readonly UK[];
161
+ };
162
+
163
+ /**
164
+ * Leaf type map — resolves primitive builders in a single indexed access
165
+ * instead of chaining conditional types.
166
+ */
167
+ interface LeafTypeMap {
168
+ string: string;
169
+ number: number;
170
+ boolean: boolean;
171
+ date: Date;
172
+ }
173
+ /**
174
+ * Resolve a leaf TypeBuilder (string/number/boolean/date) to its TS type.
175
+ */
176
+ type InferLeaf<T extends TypeBuilder> = T extends {
177
+ readonly _type: infer K extends keyof LeafTypeMap;
178
+ } ? LeafTypeMap[K] : never;
179
+ /**
180
+ * Resolve one level of wrapper (nullable/array/optional around a leaf).
181
+ */
182
+ type InferShallow<T extends TypeBuilder> = T extends NullableType<infer I extends TypeBuilder> ? InferLeaf<I> | null : T extends ArrayType<infer I extends TypeBuilder> ? InferLeaf<I>[] : T extends OptionalType<infer I extends TypeBuilder> ? InferLeaf<I> | undefined : InferLeaf<T>;
183
+ /**
184
+ * Map a TypeBuilder marker to its corresponding TypeScript type.
185
+ *
186
+ * Non-recursive: handles up to 2 levels of nesting (e.g. optional(nullable(string))).
187
+ * This avoids TS2589 when consumed through compiled .d.ts files.
188
+ */
189
+ type InferType<T extends TypeBuilder> = T extends NullableType<infer I extends TypeBuilder> ? InferShallow<I> | null : T extends ArrayType<infer I extends TypeBuilder> ? InferShallow<I>[] : T extends OptionalType<infer I extends TypeBuilder> ? InferShallow<I> | undefined : InferLeaf<T>;
190
+ type OptionalKeys<Schema extends AttributesSchema> = {
191
+ [K in keyof Schema]: Schema[K] extends OptionalType<any> ? K : never;
192
+ }[keyof Schema];
193
+ type RequiredKeys<Schema extends AttributesSchema> = Exclude<keyof Schema, OptionalKeys<Schema>>;
194
+ /**
195
+ * Map an attributes schema to a plain object type.
196
+ * Keys backed by `t.optional()` become truly optional properties.
197
+ */
198
+ type InferAttributes<Schema extends AttributesSchema> = {
199
+ [K in RequiredKeys<Schema>]: InferType<Schema[K]>;
200
+ } & {
201
+ [K in OptionalKeys<Schema>]?: InferType<Schema[K]>;
202
+ };
203
+ /**
204
+ * Simplified attribute inference without optional key splitting.
205
+ * All keys are required. Use this for payload types to reduce instantiation depth.
206
+ */
207
+ type InferAttributesSimple<Schema extends AttributesSchema> = {
208
+ [K in keyof Schema]: InferType<Schema[K]>;
209
+ };
210
+ /**
211
+ * Indexed lookup for relationship shapes — avoids conditional types.
212
+ */
213
+ interface RelationshipTypeMap<ResourceType extends string> {
214
+ one: JsonApiRelationship & {
215
+ data: {
216
+ type: ResourceType;
217
+ id: string;
218
+ } | null;
219
+ };
220
+ many: JsonApiRelationship & {
221
+ data: {
222
+ type: ResourceType;
223
+ id: string;
224
+ }[];
225
+ };
226
+ }
227
+ /**
228
+ * Map a single relationship builder to its JSON:API relationship shape.
229
+ * Uses indexed access instead of conditional types to reduce instantiation depth.
230
+ */
231
+ type InferRelationshipType<T extends RelationshipBuilder> = RelationshipTypeMap<T["type"]>[T["_rel"]];
232
+ /**
233
+ * Map a relationships schema to the full JSON:API relationships object.
234
+ */
235
+ type InferRelationships<Schema extends RelationshipsSchema> = {
236
+ [K in keyof Schema]: InferRelationshipType<Schema[K]>;
237
+ };
238
+ /**
239
+ * Infer the full JSON:API resource type from a ResourceDefinition.
240
+ * Uses direct property access instead of conditional + intersection
241
+ * to reduce type instantiation depth for external consumers.
242
+ */
243
+ type InferResource<Def extends ResourceDefinition> = {
244
+ type: Def["resourceType"];
245
+ id?: string;
246
+ attributes: InferAttributes<Def["attributes"]>;
247
+ relationships: InferRelationships<Def["relationships"]>;
248
+ links?: JsonApiLinks;
249
+ meta?: JsonApiMeta;
250
+ };
251
+ /**
252
+ * Extract just the attributes type from a ResourceDefinition.
253
+ */
254
+ type InferResourceAttributes<Def extends ResourceDefinition> = Def extends ResourceDefinition<string, infer Attrs, RelationshipsSchema> ? InferAttributes<Attrs> : never;
255
+ /**
256
+ * Extract just the relationships type from a ResourceDefinition.
257
+ */
258
+ type InferResourceRelationships<Def extends ResourceDefinition> = Def extends ResourceDefinition<string, AttributesSchema, infer Rels> ? InferRelationships<Rels> : never;
259
+ /**
260
+ * Extract the literal resource type string from a ResourceDefinition.
261
+ */
262
+ type InferResourceType<Def extends ResourceDefinition> = Def extends ResourceDefinition<infer Type, AttributesSchema, RelationshipsSchema> ? Type : never;
263
+
264
+ export { type ArrayType, type AttributesSchema, type BooleanType, type DateType, type HasManyRelationship, type HasOneRelationship, type InferAttributes, type InferAttributesSimple, type InferRelationshipType, type InferRelationships, type InferResource, type InferResourceAttributes, type InferResourceRelationships, type InferResourceType, type InferType, type JsonApiCollectionDocument, type JsonApiDocument, type JsonApiDocumentBase, type JsonApiDocumentLinks, type JsonApiEmptyDocument, type JsonApiError, type JsonApiErrorDocument, type JsonApiLinks, type JsonApiMeta, type JsonApiRelationship, type JsonApiRelationships, type JsonApiResource, type JsonApiResourceIdentifier, type JsonApiResponseDocument, type NullableType, type NumberType, type OptionalType, type RelationshipBuilder, type RelationshipsSchema, type ResourceDefinition, type ResourceOptions, type StringType, type TypeBuilder, defineResource, hasMany, hasOne, t };
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Core JSON:API specification types
3
+ * @see https://jsonapi.org/format/
4
+ */
5
+ interface JsonApiResourceIdentifier {
6
+ type: string;
7
+ id: string;
8
+ }
9
+ interface JsonApiLinks {
10
+ self?: string;
11
+ related?: string;
12
+ [key: string]: string | undefined;
13
+ }
14
+ type JsonApiMeta = Record<string, unknown>;
15
+ interface JsonApiRelationship {
16
+ data?: JsonApiResourceIdentifier | JsonApiResourceIdentifier[] | null;
17
+ links?: JsonApiLinks;
18
+ meta?: JsonApiMeta;
19
+ }
20
+ interface JsonApiRelationships {
21
+ [key: string]: JsonApiRelationship;
22
+ }
23
+ interface JsonApiResource<Attr extends Record<string, unknown> = Record<string, unknown>, Rel extends JsonApiRelationships = JsonApiRelationships> {
24
+ type: string;
25
+ id?: string;
26
+ attributes?: Attr;
27
+ relationships?: Rel;
28
+ links?: JsonApiLinks;
29
+ meta?: JsonApiMeta;
30
+ }
31
+ interface JsonApiError {
32
+ id?: string;
33
+ status?: string;
34
+ code?: string;
35
+ title?: string;
36
+ detail?: string;
37
+ source?: {
38
+ pointer?: string;
39
+ parameter?: string;
40
+ header?: string;
41
+ };
42
+ meta?: JsonApiMeta;
43
+ }
44
+ interface JsonApiDocumentLinks extends JsonApiLinks {
45
+ first?: string;
46
+ last?: string;
47
+ prev?: string;
48
+ next?: string;
49
+ }
50
+ interface JsonApiDocumentBase {
51
+ links?: JsonApiDocumentLinks;
52
+ meta?: JsonApiMeta;
53
+ jsonapi?: {
54
+ version?: string;
55
+ meta?: JsonApiMeta;
56
+ };
57
+ }
58
+
59
+ interface JsonApiDocument<T = JsonApiResource> extends JsonApiDocumentBase {
60
+ data: T;
61
+ included?: JsonApiResource[];
62
+ }
63
+ interface JsonApiCollectionDocument<T = JsonApiResource> extends JsonApiDocumentBase {
64
+ data: T[];
65
+ included?: JsonApiResource[];
66
+ }
67
+ interface JsonApiErrorDocument extends JsonApiDocumentBase {
68
+ errors: JsonApiError[];
69
+ }
70
+ interface JsonApiEmptyDocument extends JsonApiDocumentBase {
71
+ data: null;
72
+ }
73
+ type JsonApiResponseDocument<T = JsonApiResource> = JsonApiDocument<T> | JsonApiCollectionDocument<T> | JsonApiErrorDocument | JsonApiEmptyDocument;
74
+
75
+ /**
76
+ * Type builder markers and the `t` namespace
77
+ *
78
+ * Each builder is a lightweight runtime object whose TypeScript type
79
+ * drives compile-time inference via InferType<T>.
80
+ */
81
+ interface StringType {
82
+ readonly _type: "string";
83
+ }
84
+ interface NumberType {
85
+ readonly _type: "number";
86
+ }
87
+ interface BooleanType {
88
+ readonly _type: "boolean";
89
+ }
90
+ interface DateType {
91
+ readonly _type: "date";
92
+ }
93
+ interface NullableType<Inner extends TypeBuilder> {
94
+ readonly _type: "nullable";
95
+ readonly inner: Inner;
96
+ }
97
+ interface ArrayType<Inner extends TypeBuilder> {
98
+ readonly _type: "array";
99
+ readonly inner: Inner;
100
+ }
101
+ interface OptionalType<Inner extends TypeBuilder> {
102
+ readonly _type: "optional";
103
+ readonly inner: Inner;
104
+ }
105
+ type TypeBuilder = StringType | NumberType | BooleanType | DateType | NullableType<any> | ArrayType<any> | OptionalType<any>;
106
+ type AttributesSchema = Record<string, TypeBuilder>;
107
+ declare const t: {
108
+ string: () => StringType;
109
+ number: () => NumberType;
110
+ boolean: () => BooleanType;
111
+ date: () => DateType;
112
+ nullable: <T extends TypeBuilder>(inner: T) => NullableType<T>;
113
+ array: <T extends TypeBuilder>(inner: T) => ArrayType<T>;
114
+ optional: <T extends TypeBuilder>(inner: T) => OptionalType<T>;
115
+ };
116
+
117
+ /**
118
+ * Relationship definition helpers
119
+ */
120
+ interface HasOneRelationship<T extends string = string> {
121
+ readonly _rel: "one";
122
+ readonly type: T;
123
+ }
124
+ interface HasManyRelationship<T extends string = string> {
125
+ readonly _rel: "many";
126
+ readonly type: T;
127
+ }
128
+ type RelationshipBuilder = HasOneRelationship | HasManyRelationship;
129
+ type RelationshipsSchema = Record<string, RelationshipBuilder>;
130
+ declare const hasOne: <T extends string>(type: T) => HasOneRelationship<T>;
131
+ declare const hasMany: <T extends string>(type: T) => HasManyRelationship<T>;
132
+
133
+ /**
134
+ * A resource definition that captures the type-level information
135
+ * needed for inference.
136
+ */
137
+ interface ResourceDefinition<Type extends string = string, Attrs extends AttributesSchema = AttributesSchema, Rels extends RelationshipsSchema = RelationshipsSchema> {
138
+ readonly resourceType: Type;
139
+ readonly attributes: Attrs;
140
+ readonly relationships: Rels;
141
+ readonly createKeys?: readonly string[];
142
+ readonly updateKeys?: readonly string[];
143
+ }
144
+ /**
145
+ * Options for per-verb attribute narrowing.
146
+ * `create` lists which attribute keys are accepted by POST.
147
+ * `update` lists which attribute keys are accepted by PATCH.
148
+ */
149
+ interface ResourceOptions<CK extends string = string, UK extends string = string> {
150
+ readonly create: readonly CK[];
151
+ readonly update: readonly UK[];
152
+ }
153
+ /** No relationships. */
154
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema>(type: Type, attributes: Attrs): ResourceDefinition<Type, Attrs, Record<string, never>>;
155
+ /** With relationships. */
156
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema, Rels extends RelationshipsSchema>(type: Type, attributes: Attrs, relationships: Rels): ResourceDefinition<Type, Attrs, Rels>;
157
+ /** With relationships + per-verb attribute options. */
158
+ declare function defineResource<Type extends string, Attrs extends AttributesSchema, Rels extends RelationshipsSchema, CK extends keyof Attrs & string, UK extends keyof Attrs & string>(type: Type, attributes: Attrs, relationships: Rels, options: ResourceOptions<CK, UK>): ResourceDefinition<Type, Attrs, Rels> & {
159
+ readonly createKeys: readonly CK[];
160
+ readonly updateKeys: readonly UK[];
161
+ };
162
+
163
+ /**
164
+ * Leaf type map — resolves primitive builders in a single indexed access
165
+ * instead of chaining conditional types.
166
+ */
167
+ interface LeafTypeMap {
168
+ string: string;
169
+ number: number;
170
+ boolean: boolean;
171
+ date: Date;
172
+ }
173
+ /**
174
+ * Resolve a leaf TypeBuilder (string/number/boolean/date) to its TS type.
175
+ */
176
+ type InferLeaf<T extends TypeBuilder> = T extends {
177
+ readonly _type: infer K extends keyof LeafTypeMap;
178
+ } ? LeafTypeMap[K] : never;
179
+ /**
180
+ * Resolve one level of wrapper (nullable/array/optional around a leaf).
181
+ */
182
+ type InferShallow<T extends TypeBuilder> = T extends NullableType<infer I extends TypeBuilder> ? InferLeaf<I> | null : T extends ArrayType<infer I extends TypeBuilder> ? InferLeaf<I>[] : T extends OptionalType<infer I extends TypeBuilder> ? InferLeaf<I> | undefined : InferLeaf<T>;
183
+ /**
184
+ * Map a TypeBuilder marker to its corresponding TypeScript type.
185
+ *
186
+ * Non-recursive: handles up to 2 levels of nesting (e.g. optional(nullable(string))).
187
+ * This avoids TS2589 when consumed through compiled .d.ts files.
188
+ */
189
+ type InferType<T extends TypeBuilder> = T extends NullableType<infer I extends TypeBuilder> ? InferShallow<I> | null : T extends ArrayType<infer I extends TypeBuilder> ? InferShallow<I>[] : T extends OptionalType<infer I extends TypeBuilder> ? InferShallow<I> | undefined : InferLeaf<T>;
190
+ type OptionalKeys<Schema extends AttributesSchema> = {
191
+ [K in keyof Schema]: Schema[K] extends OptionalType<any> ? K : never;
192
+ }[keyof Schema];
193
+ type RequiredKeys<Schema extends AttributesSchema> = Exclude<keyof Schema, OptionalKeys<Schema>>;
194
+ /**
195
+ * Map an attributes schema to a plain object type.
196
+ * Keys backed by `t.optional()` become truly optional properties.
197
+ */
198
+ type InferAttributes<Schema extends AttributesSchema> = {
199
+ [K in RequiredKeys<Schema>]: InferType<Schema[K]>;
200
+ } & {
201
+ [K in OptionalKeys<Schema>]?: InferType<Schema[K]>;
202
+ };
203
+ /**
204
+ * Simplified attribute inference without optional key splitting.
205
+ * All keys are required. Use this for payload types to reduce instantiation depth.
206
+ */
207
+ type InferAttributesSimple<Schema extends AttributesSchema> = {
208
+ [K in keyof Schema]: InferType<Schema[K]>;
209
+ };
210
+ /**
211
+ * Indexed lookup for relationship shapes — avoids conditional types.
212
+ */
213
+ interface RelationshipTypeMap<ResourceType extends string> {
214
+ one: JsonApiRelationship & {
215
+ data: {
216
+ type: ResourceType;
217
+ id: string;
218
+ } | null;
219
+ };
220
+ many: JsonApiRelationship & {
221
+ data: {
222
+ type: ResourceType;
223
+ id: string;
224
+ }[];
225
+ };
226
+ }
227
+ /**
228
+ * Map a single relationship builder to its JSON:API relationship shape.
229
+ * Uses indexed access instead of conditional types to reduce instantiation depth.
230
+ */
231
+ type InferRelationshipType<T extends RelationshipBuilder> = RelationshipTypeMap<T["type"]>[T["_rel"]];
232
+ /**
233
+ * Map a relationships schema to the full JSON:API relationships object.
234
+ */
235
+ type InferRelationships<Schema extends RelationshipsSchema> = {
236
+ [K in keyof Schema]: InferRelationshipType<Schema[K]>;
237
+ };
238
+ /**
239
+ * Infer the full JSON:API resource type from a ResourceDefinition.
240
+ * Uses direct property access instead of conditional + intersection
241
+ * to reduce type instantiation depth for external consumers.
242
+ */
243
+ type InferResource<Def extends ResourceDefinition> = {
244
+ type: Def["resourceType"];
245
+ id?: string;
246
+ attributes: InferAttributes<Def["attributes"]>;
247
+ relationships: InferRelationships<Def["relationships"]>;
248
+ links?: JsonApiLinks;
249
+ meta?: JsonApiMeta;
250
+ };
251
+ /**
252
+ * Extract just the attributes type from a ResourceDefinition.
253
+ */
254
+ type InferResourceAttributes<Def extends ResourceDefinition> = Def extends ResourceDefinition<string, infer Attrs, RelationshipsSchema> ? InferAttributes<Attrs> : never;
255
+ /**
256
+ * Extract just the relationships type from a ResourceDefinition.
257
+ */
258
+ type InferResourceRelationships<Def extends ResourceDefinition> = Def extends ResourceDefinition<string, AttributesSchema, infer Rels> ? InferRelationships<Rels> : never;
259
+ /**
260
+ * Extract the literal resource type string from a ResourceDefinition.
261
+ */
262
+ type InferResourceType<Def extends ResourceDefinition> = Def extends ResourceDefinition<infer Type, AttributesSchema, RelationshipsSchema> ? Type : never;
263
+
264
+ export { type ArrayType, type AttributesSchema, type BooleanType, type DateType, type HasManyRelationship, type HasOneRelationship, type InferAttributes, type InferAttributesSimple, type InferRelationshipType, type InferRelationships, type InferResource, type InferResourceAttributes, type InferResourceRelationships, type InferResourceType, type InferType, type JsonApiCollectionDocument, type JsonApiDocument, type JsonApiDocumentBase, type JsonApiDocumentLinks, type JsonApiEmptyDocument, type JsonApiError, type JsonApiErrorDocument, type JsonApiLinks, type JsonApiMeta, type JsonApiRelationship, type JsonApiRelationships, type JsonApiResource, type JsonApiResourceIdentifier, type JsonApiResponseDocument, type NullableType, type NumberType, type OptionalType, type RelationshipBuilder, type RelationshipsSchema, type ResourceDefinition, type ResourceOptions, type StringType, type TypeBuilder, defineResource, hasMany, hasOne, t };
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // src/dsl/types.ts
4
+ var t = {
5
+ string: () => ({ _type: "string" }),
6
+ number: () => ({ _type: "number" }),
7
+ boolean: () => ({ _type: "boolean" }),
8
+ date: () => ({ _type: "date" }),
9
+ nullable: (inner) => ({
10
+ _type: "nullable",
11
+ inner
12
+ }),
13
+ array: (inner) => ({
14
+ _type: "array",
15
+ inner
16
+ }),
17
+ optional: (inner) => ({
18
+ _type: "optional",
19
+ inner
20
+ })
21
+ };
22
+
23
+ // src/dsl/relationships.ts
24
+ var hasOne = (type) => ({
25
+ _rel: "one",
26
+ type
27
+ });
28
+ var hasMany = (type) => ({
29
+ _rel: "many",
30
+ type
31
+ });
32
+
33
+ // src/dsl/define.ts
34
+ function defineResource(type, attributes, relationships, options) {
35
+ const def = {
36
+ resourceType: type,
37
+ attributes,
38
+ relationships: relationships ?? {}
39
+ };
40
+ if (options) {
41
+ def.createKeys = options.create;
42
+ def.updateKeys = options.update;
43
+ }
44
+ return def;
45
+ }
46
+
47
+ exports.defineResource = defineResource;
48
+ exports.hasMany = hasMany;
49
+ exports.hasOne = hasOne;
50
+ exports.t = t;
51
+ //# sourceMappingURL=index.js.map
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dsl/types.ts","../src/dsl/relationships.ts","../src/dsl/define.ts"],"names":[],"mappings":";;;AAoDO,IAAM,CAAA,GAAI;AAAA,EACf,MAAA,EAAQ,OAAmB,EAAE,KAAA,EAAO,QAAA,EAAkB,CAAA;AAAA,EACtD,MAAA,EAAQ,OAAmB,EAAE,KAAA,EAAO,QAAA,EAAkB,CAAA;AAAA,EACtD,OAAA,EAAS,OAAoB,EAAE,KAAA,EAAO,SAAA,EAAmB,CAAA;AAAA,EACzD,IAAA,EAAM,OAAiB,EAAE,KAAA,EAAO,MAAA,EAAgB,CAAA;AAAA,EAEhD,QAAA,EAAU,CAAwB,KAAA,MAA+B;AAAA,IAC/D,KAAA,EAAO,UAAA;AAAA,IACP;AAAA,GACF,CAAA;AAAA,EAEA,KAAA,EAAO,CAAwB,KAAA,MAA4B;AAAA,IACzD,KAAA,EAAO,OAAA;AAAA,IACP;AAAA,GACF,CAAA;AAAA,EAEA,QAAA,EAAU,CAAwB,KAAA,MAA+B;AAAA,IAC/D,KAAA,EAAO,UAAA;AAAA,IACP;AAAA,GACF;AACF;;;ACtDO,IAAM,MAAA,GAAS,CAAmB,IAAA,MAAoC;AAAA,EAC3E,IAAA,EAAM,KAAA;AAAA,EACN;AACF,CAAA;AAEO,IAAM,OAAA,GAAU,CAAmB,IAAA,MAAqC;AAAA,EAC7E,IAAA,EAAM,MAAA;AAAA,EACN;AACF,CAAA;;;AC+CO,SAAS,cAAA,CACd,IAAA,EACA,UAAA,EACA,aAAA,EACA,OAAA,EACoB;AACpB,EAAA,MAAM,GAAA,GAA+B;AAAA,IACnC,YAAA,EAAc,IAAA;AAAA,IACd,UAAA;AAAA,IACA,aAAA,EAAe,iBAAiB;AAAC,GACnC;AACA,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,GAAA,CAAI,aAAa,OAAA,CAAQ,MAAA;AACzB,IAAA,GAAA,CAAI,aAAa,OAAA,CAAQ,MAAA;AAAA,EAC3B;AACA,EAAA,OAAO,GAAA;AACT","file":"index.js","sourcesContent":["/**\n * Type builder markers and the `t` namespace\n *\n * Each builder is a lightweight runtime object whose TypeScript type\n * drives compile-time inference via InferType<T>.\n */\n\nexport interface StringType {\n readonly _type: \"string\";\n}\n\nexport interface NumberType {\n readonly _type: \"number\";\n}\n\nexport interface BooleanType {\n readonly _type: \"boolean\";\n}\n\nexport interface DateType {\n readonly _type: \"date\";\n}\n\nexport interface NullableType<Inner extends TypeBuilder> {\n readonly _type: \"nullable\";\n readonly inner: Inner;\n}\n\nexport interface ArrayType<Inner extends TypeBuilder> {\n readonly _type: \"array\";\n readonly inner: Inner;\n}\n\nexport interface OptionalType<Inner extends TypeBuilder> {\n readonly _type: \"optional\";\n readonly inner: Inner;\n}\n\n// `any` is intentional: NullableType<TypeBuilder> would be circular.\n// The generic parameter on each container type still enforces correctness\n// at the call site (t.nullable(), t.array(), t.optional()).\nexport type TypeBuilder =\n | StringType\n | NumberType\n | BooleanType\n | DateType\n | NullableType<any>\n | ArrayType<any>\n | OptionalType<any>;\n\nexport type AttributesSchema = Record<string, TypeBuilder>;\n\nexport const t = {\n string: (): StringType => ({ _type: \"string\" as const }),\n number: (): NumberType => ({ _type: \"number\" as const }),\n boolean: (): BooleanType => ({ _type: \"boolean\" as const }),\n date: (): DateType => ({ _type: \"date\" as const }),\n\n nullable: <T extends TypeBuilder>(inner: T): NullableType<T> => ({\n _type: \"nullable\" as const,\n inner,\n }),\n\n array: <T extends TypeBuilder>(inner: T): ArrayType<T> => ({\n _type: \"array\" as const,\n inner,\n }),\n\n optional: <T extends TypeBuilder>(inner: T): OptionalType<T> => ({\n _type: \"optional\" as const,\n inner,\n }),\n};\n","/**\n * Relationship definition helpers\n */\n\nexport interface HasOneRelationship<T extends string = string> {\n readonly _rel: \"one\";\n readonly type: T;\n}\n\nexport interface HasManyRelationship<T extends string = string> {\n readonly _rel: \"many\";\n readonly type: T;\n}\n\nexport type RelationshipBuilder = HasOneRelationship | HasManyRelationship;\n\nexport type RelationshipsSchema = Record<string, RelationshipBuilder>;\n\nexport const hasOne = <T extends string>(type: T): HasOneRelationship<T> => ({\n _rel: \"one\" as const,\n type,\n});\n\nexport const hasMany = <T extends string>(type: T): HasManyRelationship<T> => ({\n _rel: \"many\" as const,\n type,\n});\n","import type { AttributesSchema } from \"./types\";\nimport type { RelationshipsSchema } from \"./relationships\";\n\n/**\n * A resource definition that captures the type-level information\n * needed for inference.\n */\nexport interface ResourceDefinition<\n Type extends string = string,\n Attrs extends AttributesSchema = AttributesSchema,\n Rels extends RelationshipsSchema = RelationshipsSchema,\n> {\n readonly resourceType: Type;\n readonly attributes: Attrs;\n readonly relationships: Rels;\n readonly createKeys?: readonly string[];\n readonly updateKeys?: readonly string[];\n}\n\n/**\n * Options for per-verb attribute narrowing.\n * `create` lists which attribute keys are accepted by POST.\n * `update` lists which attribute keys are accepted by PATCH.\n */\nexport interface ResourceOptions<\n CK extends string = string,\n UK extends string = string,\n> {\n readonly create: readonly CK[];\n readonly update: readonly UK[];\n}\n\n// ── Overloads ────────────────────────────────────────────────────────────\n\n/** No relationships. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema\n>(\n type: Type,\n attributes: Attrs,\n): ResourceDefinition<Type, Attrs, Record<string, never>>;\n\n/** With relationships. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema,\n Rels extends RelationshipsSchema\n>(\n type: Type,\n attributes: Attrs,\n relationships: Rels,\n): ResourceDefinition<Type, Attrs, Rels>;\n\n/** With relationships + per-verb attribute options. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema,\n Rels extends RelationshipsSchema,\n CK extends keyof Attrs & string,\n UK extends keyof Attrs & string,\n>(\n type: Type,\n attributes: Attrs,\n relationships: Rels,\n options: ResourceOptions<CK, UK>,\n): ResourceDefinition<Type, Attrs, Rels> & {\n readonly createKeys: readonly CK[];\n readonly updateKeys: readonly UK[];\n};\n\n// ── Implementation ───────────────────────────────────────────────────────\n\nexport function defineResource(\n type: string,\n attributes: AttributesSchema,\n relationships?: RelationshipsSchema,\n options?: ResourceOptions,\n): ResourceDefinition {\n const def: Record<string, unknown> = {\n resourceType: type,\n attributes,\n relationships: relationships ?? {},\n };\n if (options) {\n def.createKeys = options.create;\n def.updateKeys = options.update;\n }\n return def as unknown as ResourceDefinition;\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,47 @@
1
+ // src/dsl/types.ts
2
+ var t = {
3
+ string: () => ({ _type: "string" }),
4
+ number: () => ({ _type: "number" }),
5
+ boolean: () => ({ _type: "boolean" }),
6
+ date: () => ({ _type: "date" }),
7
+ nullable: (inner) => ({
8
+ _type: "nullable",
9
+ inner
10
+ }),
11
+ array: (inner) => ({
12
+ _type: "array",
13
+ inner
14
+ }),
15
+ optional: (inner) => ({
16
+ _type: "optional",
17
+ inner
18
+ })
19
+ };
20
+
21
+ // src/dsl/relationships.ts
22
+ var hasOne = (type) => ({
23
+ _rel: "one",
24
+ type
25
+ });
26
+ var hasMany = (type) => ({
27
+ _rel: "many",
28
+ type
29
+ });
30
+
31
+ // src/dsl/define.ts
32
+ function defineResource(type, attributes, relationships, options) {
33
+ const def = {
34
+ resourceType: type,
35
+ attributes,
36
+ relationships: relationships ?? {}
37
+ };
38
+ if (options) {
39
+ def.createKeys = options.create;
40
+ def.updateKeys = options.update;
41
+ }
42
+ return def;
43
+ }
44
+
45
+ export { defineResource, hasMany, hasOne, t };
46
+ //# sourceMappingURL=index.mjs.map
47
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dsl/types.ts","../src/dsl/relationships.ts","../src/dsl/define.ts"],"names":[],"mappings":";AAoDO,IAAM,CAAA,GAAI;AAAA,EACf,MAAA,EAAQ,OAAmB,EAAE,KAAA,EAAO,QAAA,EAAkB,CAAA;AAAA,EACtD,MAAA,EAAQ,OAAmB,EAAE,KAAA,EAAO,QAAA,EAAkB,CAAA;AAAA,EACtD,OAAA,EAAS,OAAoB,EAAE,KAAA,EAAO,SAAA,EAAmB,CAAA;AAAA,EACzD,IAAA,EAAM,OAAiB,EAAE,KAAA,EAAO,MAAA,EAAgB,CAAA;AAAA,EAEhD,QAAA,EAAU,CAAwB,KAAA,MAA+B;AAAA,IAC/D,KAAA,EAAO,UAAA;AAAA,IACP;AAAA,GACF,CAAA;AAAA,EAEA,KAAA,EAAO,CAAwB,KAAA,MAA4B;AAAA,IACzD,KAAA,EAAO,OAAA;AAAA,IACP;AAAA,GACF,CAAA;AAAA,EAEA,QAAA,EAAU,CAAwB,KAAA,MAA+B;AAAA,IAC/D,KAAA,EAAO,UAAA;AAAA,IACP;AAAA,GACF;AACF;;;ACtDO,IAAM,MAAA,GAAS,CAAmB,IAAA,MAAoC;AAAA,EAC3E,IAAA,EAAM,KAAA;AAAA,EACN;AACF,CAAA;AAEO,IAAM,OAAA,GAAU,CAAmB,IAAA,MAAqC;AAAA,EAC7E,IAAA,EAAM,MAAA;AAAA,EACN;AACF,CAAA;;;AC+CO,SAAS,cAAA,CACd,IAAA,EACA,UAAA,EACA,aAAA,EACA,OAAA,EACoB;AACpB,EAAA,MAAM,GAAA,GAA+B;AAAA,IACnC,YAAA,EAAc,IAAA;AAAA,IACd,UAAA;AAAA,IACA,aAAA,EAAe,iBAAiB;AAAC,GACnC;AACA,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,GAAA,CAAI,aAAa,OAAA,CAAQ,MAAA;AACzB,IAAA,GAAA,CAAI,aAAa,OAAA,CAAQ,MAAA;AAAA,EAC3B;AACA,EAAA,OAAO,GAAA;AACT","file":"index.mjs","sourcesContent":["/**\n * Type builder markers and the `t` namespace\n *\n * Each builder is a lightweight runtime object whose TypeScript type\n * drives compile-time inference via InferType<T>.\n */\n\nexport interface StringType {\n readonly _type: \"string\";\n}\n\nexport interface NumberType {\n readonly _type: \"number\";\n}\n\nexport interface BooleanType {\n readonly _type: \"boolean\";\n}\n\nexport interface DateType {\n readonly _type: \"date\";\n}\n\nexport interface NullableType<Inner extends TypeBuilder> {\n readonly _type: \"nullable\";\n readonly inner: Inner;\n}\n\nexport interface ArrayType<Inner extends TypeBuilder> {\n readonly _type: \"array\";\n readonly inner: Inner;\n}\n\nexport interface OptionalType<Inner extends TypeBuilder> {\n readonly _type: \"optional\";\n readonly inner: Inner;\n}\n\n// `any` is intentional: NullableType<TypeBuilder> would be circular.\n// The generic parameter on each container type still enforces correctness\n// at the call site (t.nullable(), t.array(), t.optional()).\nexport type TypeBuilder =\n | StringType\n | NumberType\n | BooleanType\n | DateType\n | NullableType<any>\n | ArrayType<any>\n | OptionalType<any>;\n\nexport type AttributesSchema = Record<string, TypeBuilder>;\n\nexport const t = {\n string: (): StringType => ({ _type: \"string\" as const }),\n number: (): NumberType => ({ _type: \"number\" as const }),\n boolean: (): BooleanType => ({ _type: \"boolean\" as const }),\n date: (): DateType => ({ _type: \"date\" as const }),\n\n nullable: <T extends TypeBuilder>(inner: T): NullableType<T> => ({\n _type: \"nullable\" as const,\n inner,\n }),\n\n array: <T extends TypeBuilder>(inner: T): ArrayType<T> => ({\n _type: \"array\" as const,\n inner,\n }),\n\n optional: <T extends TypeBuilder>(inner: T): OptionalType<T> => ({\n _type: \"optional\" as const,\n inner,\n }),\n};\n","/**\n * Relationship definition helpers\n */\n\nexport interface HasOneRelationship<T extends string = string> {\n readonly _rel: \"one\";\n readonly type: T;\n}\n\nexport interface HasManyRelationship<T extends string = string> {\n readonly _rel: \"many\";\n readonly type: T;\n}\n\nexport type RelationshipBuilder = HasOneRelationship | HasManyRelationship;\n\nexport type RelationshipsSchema = Record<string, RelationshipBuilder>;\n\nexport const hasOne = <T extends string>(type: T): HasOneRelationship<T> => ({\n _rel: \"one\" as const,\n type,\n});\n\nexport const hasMany = <T extends string>(type: T): HasManyRelationship<T> => ({\n _rel: \"many\" as const,\n type,\n});\n","import type { AttributesSchema } from \"./types\";\nimport type { RelationshipsSchema } from \"./relationships\";\n\n/**\n * A resource definition that captures the type-level information\n * needed for inference.\n */\nexport interface ResourceDefinition<\n Type extends string = string,\n Attrs extends AttributesSchema = AttributesSchema,\n Rels extends RelationshipsSchema = RelationshipsSchema,\n> {\n readonly resourceType: Type;\n readonly attributes: Attrs;\n readonly relationships: Rels;\n readonly createKeys?: readonly string[];\n readonly updateKeys?: readonly string[];\n}\n\n/**\n * Options for per-verb attribute narrowing.\n * `create` lists which attribute keys are accepted by POST.\n * `update` lists which attribute keys are accepted by PATCH.\n */\nexport interface ResourceOptions<\n CK extends string = string,\n UK extends string = string,\n> {\n readonly create: readonly CK[];\n readonly update: readonly UK[];\n}\n\n// ── Overloads ────────────────────────────────────────────────────────────\n\n/** No relationships. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema\n>(\n type: Type,\n attributes: Attrs,\n): ResourceDefinition<Type, Attrs, Record<string, never>>;\n\n/** With relationships. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema,\n Rels extends RelationshipsSchema\n>(\n type: Type,\n attributes: Attrs,\n relationships: Rels,\n): ResourceDefinition<Type, Attrs, Rels>;\n\n/** With relationships + per-verb attribute options. */\nexport function defineResource<\n Type extends string,\n Attrs extends AttributesSchema,\n Rels extends RelationshipsSchema,\n CK extends keyof Attrs & string,\n UK extends keyof Attrs & string,\n>(\n type: Type,\n attributes: Attrs,\n relationships: Rels,\n options: ResourceOptions<CK, UK>,\n): ResourceDefinition<Type, Attrs, Rels> & {\n readonly createKeys: readonly CK[];\n readonly updateKeys: readonly UK[];\n};\n\n// ── Implementation ───────────────────────────────────────────────────────\n\nexport function defineResource(\n type: string,\n attributes: AttributesSchema,\n relationships?: RelationshipsSchema,\n options?: ResourceOptions,\n): ResourceDefinition {\n const def: Record<string, unknown> = {\n resourceType: type,\n attributes,\n relationships: relationships ?? {},\n };\n if (options) {\n def.createKeys = options.create;\n def.updateKeys = options.update;\n }\n return def as unknown as ResourceDefinition;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@leodamours/jsonapi-dsl",
3
+ "version": "1.0.0",
4
+ "description": "A TypeScript-first DSL for JSON:API — typed resource definitions, relationships, and inference",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run"
20
+ },
21
+ "keywords": [
22
+ "jsonapi",
23
+ "json:api",
24
+ "typescript",
25
+ "types",
26
+ "type-safe",
27
+ "type-inference",
28
+ "schema",
29
+ "dsl"
30
+ ],
31
+ "author": "Leo Damours",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/LeoDamours/json-api-client.git",
36
+ "directory": "packages/dsl"
37
+ },
38
+ "homepage": "https://github.com/LeoDamours/json-api-client/tree/main/packages/dsl",
39
+ "bugs": "https://github.com/LeoDamours/json-api-client/issues",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.4.0",
45
+ "tsup": "^8.0.0"
46
+ },
47
+ "files": [
48
+ "dist/**/*",
49
+ "LICENSE"
50
+ ]
51
+ }