@game-infra/story-schemas 0.2.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) 2026 Igor Savin
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,133 @@
1
+ # @game-infra/story-schemas
2
+
3
+ API contract for authored design content and story mode: valibot schemas plus [toad-contracts](https://github.com/kibertoad/toad-contracts) `defineApiContract` definitions. Covers worlds, the content written against them (locations, characters, skill definitions, lore entries), the story arcs chosen for a world, and the branching beats those arcs are told in. Builds on `@game-infra/api-schemas-core` for the shared success-response wrapper.
4
+
5
+ Published to npm. Also consumable as sibling source (`exports` points at `src/`).
6
+
7
+ ## Install
8
+
9
+ ```jsonc
10
+ // pnpm sibling checkout
11
+ "@game-infra/story-schemas": "link:../../game-infra/packages/schemas/story-schemas"
12
+ // or from npm
13
+ "@game-infra/story-schemas": "^0.1.0"
14
+ ```
15
+
16
+ `valibot` is a peer dependency (`^1.4.0`); the consumer provides it. `@game-infra/api-schemas-core` is a runtime dependency (re-uses `createSuccessResponseSchema`).
17
+
18
+ ## One resource, not a family
19
+
20
+ All of it is the same shape — a keyed JSON payload, scoped to a parent, tagged with a type — so it is **one** resource with five endpoints rather than a CRUD family per kind. `contentType` and `scopeId` do the work a separate resource per kind would otherwise do:
21
+
22
+ | `contentType` | `scopeId` | `contentId` |
23
+ | ------------------------------------------------------------------------------------ | ---------- | ----------------------- |
24
+ | `world` | `-` | the world id |
25
+ | `locations`, `characters`, `events`, `stats`, `skills`, `perks`, `resources`, `lore` | a world id | the entity id |
26
+ | `story-arc` | a world id | the story id |
27
+ | `story-node` | a story id | the trail of choice ids |
28
+
29
+ `data` is an opaque JSON string. The service stores and indexes it, never parses it — a game's design vocabulary changes far more often than this contract should, which is the same rule `GameEntitySaveRequestSchema` already follows in `api-schemas-core`. `contentType` is likewise plain `string()`; `DESIGN_CONTENT_TYPES` is the suggested vocabulary, not a closed one.
30
+
31
+ ## Usage
32
+
33
+ ```ts
34
+ import { designContentListContract, designContentLoadContract } from "@game-infra/story-schemas";
35
+ import { mapApiContractToPath } from "@toad-contracts/valibot";
36
+
37
+ designContentListContract.pathResolver({ gameId: "g1" }); // '/games/g1/design-content'
38
+ mapApiContractToPath(designContentLoadContract);
39
+ // '/games/:gameId/design-content/:contentType/:scopeId/:contentId'
40
+ ```
41
+
42
+ Register on a Hono service with `@toad-contracts/hono` (`buildHonoRoute`) and call from a frontend with `@toad-contracts/frontend-http-client` (`sendByApiContract`) — both drive off the same contract objects.
43
+
44
+ ### The five endpoints
45
+
46
+ | Contract | Method + path | What it is for |
47
+ | ------------------------------- | ------------------------------------------- | -------------------------------------------------- |
48
+ | `designContentListContract` | `GET /games/:gameId/design-content` | every listing, narrowed by `contentType`/`scopeId` |
49
+ | `designContentLoadContract` | `GET …/:contentType/:scopeId/:contentId` | one row by key |
50
+ | `designContentSaveContract` | `POST /games/:gameId/design-content` | upsert one row |
51
+ | `designContentBulkSaveContract` | `POST /games/:gameId/design-content/bulk` | upsert many, optionally as a snapshot |
52
+ | `designContentDeleteContract` | `DELETE …/:contentType/:scopeId/:contentId` | delete one row, with an optional cascade |
53
+
54
+ The listing answers every question story mode asks:
55
+
56
+ ```ts
57
+ // the worlds to choose between
58
+ { contentType: "world" }
59
+ // a world's whole design document, payloads included
60
+ { scopeId: "the-ash-reckoning", includeData: "true" }
61
+ // the arcs offered for that world
62
+ { contentType: "story-arc", scopeId: "the-ash-reckoning" }
63
+ // every beat already written for an arc — the map of what is pregenerated
64
+ { contentType: "story-node", scopeId: "the-quiet-forge-answers", includeData: "true" }
65
+ ```
66
+
67
+ `includeData` is off by default: a picker needs names and counts, not every character's biography.
68
+
69
+ ### Snapshots and cascades
70
+
71
+ `designContentBulkSaveContract` takes a `replace` list. Each entry clears a scope (or one type within it) before the write, which is what makes a bulk save a snapshot rather than a merge — without it, content the author deleted locally would survive on the server forever.
72
+
73
+ `designContentDeleteContract` takes `?cascade=`:
74
+
75
+ - `none` (default) — the row alone.
76
+ - `prefix` — the row and every row whose `contentId` sits under it. Discarding a story branch takes its descendants with it this way.
77
+ - `scope` — the row and everything scoped to it, transitively. Deleting a world also removes its content, its arcs, and their beats.
78
+
79
+ ### Payload shapes — the event editor's, not new ones
80
+
81
+ A branching story and a multi-stage event are the same object seen from two angles: both are prose with gated, consequential branches out of it. So a story beat **is** an `EncounterStageDTO` and a branch **is** a `ChoiceDefinitionDTO`, straight from `@game-infra/event-schemas`:
82
+
83
+ | Story mode | event-editor DTO |
84
+ | ---------- | --------------------------------------------------------- |
85
+ | arc | `EventDefinitionDTO` header (multi-stage) + `hook`/`tags` |
86
+ | beat | `EncounterStageDTO` + `model`/`origin`/`revision`/`notes` |
87
+ | branch | `ChoiceDefinitionDTO`, unchanged |
88
+
89
+ A branch therefore carries `conditionsToShow`, `conditionsToEnable`, `consumableConditions`, `effects` and an outcome config exactly as an event's choice does, and a finished arc assembles into an `event_definitions` row by collecting its beats into `stages` — a copy, not a translation.
90
+
91
+ `data` is opaque to the _service_, but two clients writing and reading the same rows still have to agree, so the shapes ship alongside the contract and are parsed client-side:
92
+
93
+ ```ts
94
+ import { StoryBeatPayloadSchema } from "@game-infra/story-schemas";
95
+ import { safeParse } from "valibot";
96
+
97
+ const parsed = safeParse(StoryBeatPayloadSchema, JSON.parse(row.data ?? "{}"));
98
+ if (parsed.success) console.log(parsed.output.description, parsed.output.choices);
99
+ ```
100
+
101
+ `WorldPayloadSchema`, `StoryArcPayloadSchema` and `StoryBeatPayloadSchema` are exported for this, along with the underlying `ChoiceDefinitionDTOSchema`, `PreconditionDTOSchema`, `ActivationDTOSchema` and `ConsumableConditionDTOSchema` so a story client needs one import rather than two. They are deliberately _not_ attached to any endpoint.
102
+
103
+ **Use the lenient DTOs, not the strict ones.** `StrictPreconditionDTOSchema` and `StrictActivationDTOSchema` enumerate one game's vocabulary (`strength`, `mana`, `LearnSpell`, `SpecificLecture`). A design world declares its own stats, skills, perks and resources, so a story client uses the lenient `{ type, params }` forms and validates `type` and the ids in `params` against the world it belongs to.
104
+
105
+ ### Beat addressing
106
+
107
+ A beat's identity is the trail of choices taken to reach it — that is what makes "has this branch been travelled before?" a lookup rather than a search. The client minting ids and the service sweeping subtrees import the same helpers:
108
+
109
+ ```ts
110
+ import { storyNodeId, childStoryNodeId, isStoryNodeAtOrBelow } from "@game-infra/story-schemas";
111
+
112
+ storyNodeId([]); // 'root'
113
+ childStoryNodeId("root", "take-the-tunnel"); // 'root.take-the-tunnel'
114
+ isStoryNodeAtOrBelow("root.take.left", "root.take"); // true
115
+ isStoryNodeAtOrBelow("root.taken", "root.take"); // false
116
+ ```
117
+
118
+ Segments join with `.` rather than `/` because a beat id travels as a single URL path segment, which a slash would split; the dot is also what `cascade=prefix` sweeps on.
119
+
120
+ ## Extension points
121
+
122
+ - **`contentType`**: typed `string()`; `DESIGN_CONTENT_TYPES` is the suggested vocabulary and a game re-tightens it in its own validation layer.
123
+ - **`data`**: opaque to the contract, so a game evolves its design document without a contract bump. Add a payload schema next to the existing three when a new type needs one.
124
+ - **`origin`**: a beat is `generated` or `authored` — the latter is a branch a player wrote themselves.
125
+ - **Condition and effect vocabulary**: `type` is free text in the lenient DTOs, so each game declares the set it evaluates and drops the rest.
126
+
127
+ ## Dependencies
128
+
129
+ `@game-infra/api-schemas-core` (the success-response wrapper), `@game-infra/event-schemas` (the stage and choice DTOs), `@toad-contracts/core`, `@toad-contracts/valibot`; peer `valibot`.
130
+
131
+ ## Consumers
132
+
133
+ `services/game-content-service` (the Hono service registering these routes) and the design/story clients that call them — currently the Fluid Emerald game editor.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Rows for a game, narrowed by type and scope.
3
+ *
4
+ * This one endpoint answers every listing story mode needs: the worlds to
5
+ * choose between (`contentType=world`), a world's whole design document
6
+ * (`scopeId=<worldId>&includeData=true`), the arcs offered for it
7
+ * (`contentType=story-arc&scopeId=<worldId>`), and the beats already written
8
+ * for an arc (`contentType=story-node&scopeId=<storyId>&includeData=true`) —
9
+ * which is the map of what is pregenerated and must not be generated again.
10
+ */
11
+ export declare const designContentListContract: {
12
+ readonly method: "get";
13
+ readonly requestPathParamsSchema: import("valibot").ObjectSchema<{
14
+ readonly gameId: import("valibot").StringSchema<undefined>;
15
+ }, undefined> & import("@toad-contracts/valibot").StandardObjectKeysV1<unknown, unknown>;
16
+ readonly requestQuerySchema: import("valibot").ObjectSchema<{
17
+ readonly contentType: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
18
+ readonly scopeId: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
19
+ readonly includeData: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
20
+ }, undefined>;
21
+ readonly pathResolver: ({ gameId }: {
22
+ gameId: string;
23
+ }) => string;
24
+ readonly responsesByStatusCode: {
25
+ 200: import("valibot").ObjectSchema<{
26
+ readonly success: import("valibot").BooleanSchema<undefined>;
27
+ readonly error: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
28
+ } & {
29
+ items: import("valibot").OptionalSchema<import("valibot").ArraySchema<import("valibot").ObjectSchema<{
30
+ readonly contentType: import("valibot").StringSchema<undefined>;
31
+ readonly scopeId: import("valibot").StringSchema<undefined>;
32
+ readonly contentId: import("valibot").StringSchema<undefined>;
33
+ readonly name: import("valibot").StringSchema<undefined>;
34
+ readonly data: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
35
+ readonly createdAt: import("valibot").StringSchema<undefined>;
36
+ readonly updatedAt: import("valibot").StringSchema<undefined>;
37
+ }, undefined>, undefined>, undefined>;
38
+ }, undefined>;
39
+ };
40
+ };
41
+ /** One row by key, or `item: undefined` when nothing is stored there. */
42
+ export declare const designContentLoadContract: {
43
+ readonly method: "get";
44
+ readonly requestPathParamsSchema: import("valibot").ObjectSchema<{
45
+ readonly gameId: import("valibot").StringSchema<undefined>;
46
+ readonly contentType: import("valibot").StringSchema<undefined>;
47
+ readonly scopeId: import("valibot").StringSchema<undefined>;
48
+ readonly contentId: import("valibot").StringSchema<undefined>;
49
+ }, undefined> & import("@toad-contracts/valibot").StandardObjectKeysV1<unknown, unknown>;
50
+ readonly pathResolver: ({ gameId, contentType, scopeId, contentId }: {
51
+ gameId: string;
52
+ contentType: string;
53
+ scopeId: string;
54
+ contentId: string;
55
+ }) => string;
56
+ readonly responsesByStatusCode: {
57
+ 200: import("valibot").ObjectSchema<{
58
+ readonly success: import("valibot").BooleanSchema<undefined>;
59
+ readonly error: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
60
+ } & {
61
+ item: import("valibot").OptionalSchema<import("valibot").ObjectSchema<{
62
+ readonly contentType: import("valibot").StringSchema<undefined>;
63
+ readonly scopeId: import("valibot").StringSchema<undefined>;
64
+ readonly contentId: import("valibot").StringSchema<undefined>;
65
+ readonly name: import("valibot").StringSchema<undefined>;
66
+ readonly data: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
67
+ readonly createdAt: import("valibot").StringSchema<undefined>;
68
+ readonly updatedAt: import("valibot").StringSchema<undefined>;
69
+ }, undefined>, undefined>;
70
+ }, undefined>;
71
+ };
72
+ };
73
+ /** Create or replace one row. Re-saving the same key keeps its `createdAt`. */
74
+ export declare const designContentSaveContract: {
75
+ readonly method: "post";
76
+ readonly requestPathParamsSchema: import("valibot").ObjectSchema<{
77
+ readonly gameId: import("valibot").StringSchema<undefined>;
78
+ }, undefined> & import("@toad-contracts/valibot").StandardObjectKeysV1<unknown, unknown>;
79
+ readonly pathResolver: ({ gameId }: {
80
+ gameId: string;
81
+ }) => string;
82
+ readonly requestBodySchema: import("valibot").ObjectSchema<{
83
+ readonly contentType: import("valibot").StringSchema<undefined>;
84
+ readonly scopeId: import("valibot").StringSchema<undefined>;
85
+ readonly contentId: import("valibot").StringSchema<undefined>;
86
+ readonly name: import("valibot").StringSchema<undefined>;
87
+ readonly data: import("valibot").StringSchema<undefined>;
88
+ }, undefined>;
89
+ readonly responsesByStatusCode: {
90
+ 200: import("valibot").ObjectSchema<{
91
+ readonly success: import("valibot").BooleanSchema<undefined>;
92
+ readonly error: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
93
+ } & {
94
+ contentId: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
95
+ }, undefined>;
96
+ };
97
+ };
98
+ /**
99
+ * Write many rows at once; `replace` makes it a snapshot rather than a merge,
100
+ * which is how a whole design document lands in one request.
101
+ */
102
+ export declare const designContentBulkSaveContract: {
103
+ readonly method: "post";
104
+ readonly requestPathParamsSchema: import("valibot").ObjectSchema<{
105
+ readonly gameId: import("valibot").StringSchema<undefined>;
106
+ }, undefined> & import("@toad-contracts/valibot").StandardObjectKeysV1<unknown, unknown>;
107
+ readonly pathResolver: ({ gameId }: {
108
+ gameId: string;
109
+ }) => string;
110
+ readonly requestBodySchema: import("valibot").ObjectSchema<{
111
+ readonly items: import("valibot").ArraySchema<import("valibot").ObjectSchema<{
112
+ readonly contentType: import("valibot").StringSchema<undefined>;
113
+ readonly scopeId: import("valibot").StringSchema<undefined>;
114
+ readonly contentId: import("valibot").StringSchema<undefined>;
115
+ readonly name: import("valibot").StringSchema<undefined>;
116
+ readonly data: import("valibot").StringSchema<undefined>;
117
+ }, undefined>, undefined>;
118
+ readonly replace: import("valibot").OptionalSchema<import("valibot").ArraySchema<import("valibot").ObjectSchema<{
119
+ readonly scopeId: import("valibot").StringSchema<undefined>;
120
+ readonly contentType: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
121
+ }, undefined>, undefined>, undefined>;
122
+ }, undefined>;
123
+ readonly responsesByStatusCode: {
124
+ 200: import("valibot").ObjectSchema<{
125
+ readonly success: import("valibot").BooleanSchema<undefined>;
126
+ readonly error: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
127
+ } & {
128
+ savedCount: import("valibot").OptionalSchema<import("valibot").NumberSchema<undefined>, undefined>;
129
+ removedCount: import("valibot").OptionalSchema<import("valibot").NumberSchema<undefined>, undefined>;
130
+ }, undefined>;
131
+ };
132
+ };
133
+ /**
134
+ * Delete one row, and — depending on `?cascade=` — what hangs off it:
135
+ * `prefix` also removes the rows whose `contentId` sits under this one (a story
136
+ * branch and its descendants), `scope` also removes everything scoped to it,
137
+ * transitively (a world, its content, its arcs, and their beats).
138
+ */
139
+ export declare const designContentDeleteContract: {
140
+ readonly method: "delete";
141
+ readonly requestPathParamsSchema: import("valibot").ObjectSchema<{
142
+ readonly gameId: import("valibot").StringSchema<undefined>;
143
+ readonly contentType: import("valibot").StringSchema<undefined>;
144
+ readonly scopeId: import("valibot").StringSchema<undefined>;
145
+ readonly contentId: import("valibot").StringSchema<undefined>;
146
+ }, undefined> & import("@toad-contracts/valibot").StandardObjectKeysV1<unknown, unknown>;
147
+ readonly requestQuerySchema: import("valibot").ObjectSchema<{
148
+ readonly cascade: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
149
+ }, undefined>;
150
+ readonly pathResolver: ({ gameId, contentType, scopeId, contentId }: {
151
+ gameId: string;
152
+ contentType: string;
153
+ scopeId: string;
154
+ contentId: string;
155
+ }) => string;
156
+ readonly responsesByStatusCode: {
157
+ 200: import("valibot").ObjectSchema<{
158
+ readonly success: import("valibot").BooleanSchema<undefined>;
159
+ readonly error: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
160
+ } & {
161
+ deletedCount: import("valibot").OptionalSchema<import("valibot").NumberSchema<undefined>, undefined>;
162
+ }, undefined>;
163
+ };
164
+ };
165
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;QAKX,GAAG;;;;;;;;;;;;;;;CAC5B,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;QAKX,GAAG;;;;;;;;;;;;;;;CAC5B,CAAC;AAEH,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;QAKX,GAAG;;;;;;;CAC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;QAKf,GAAG;;;;;;;;CAC5B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;QAMb,GAAG;;;;;;;CAC5B,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { defineApiContract, withObjectKeys } from "@toad-contracts/valibot";
2
+ import { object, optional, string } from "valibot";
3
+ import { DesignContentBulkSaveRequestSchema, DesignContentBulkSaveResponseSchema, DesignContentDeleteResponseSchema, DesignContentListResponseSchema, DesignContentLoadResponseSchema, DesignContentSaveRequestSchema, DesignContentSaveResponseSchema, } from "./designContent.js";
4
+ /**
5
+ * Typed endpoint contracts for authored design content — worlds, the content
6
+ * written against them, story arcs, and story beats.
7
+ *
8
+ * There are five, not one family per type, because all of it is the same shape:
9
+ * a keyed JSON payload, scoped to a parent, tagged with a type. `contentType`
10
+ * and `scopeId` do the work that a separate resource per kind would otherwise
11
+ * do, and `data` stays opaque to the service.
12
+ *
13
+ * Register them on a Hono service with `@toad-contracts/hono` (`buildHonoRoute`)
14
+ * and call them from a client with `@toad-contracts/frontend-http-client`
15
+ * (`sendByApiContract`) — both drive off these same objects.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { designContentListContract, designContentLoadContract } from '@game-infra/story-schemas'
20
+ * import { mapApiContractToPath } from '@toad-contracts/valibot'
21
+ *
22
+ * designContentListContract.pathResolver({ gameId: 'g1' }) // '/games/g1/design-content'
23
+ * designContentLoadContract.pathResolver({
24
+ * gameId: 'g1', contentType: 'story-node', scopeId: 's1', contentId: 'root',
25
+ * }) // '/games/g1/design-content/story-node/s1/root'
26
+ * mapApiContractToPath(designContentLoadContract)
27
+ * // '/games/:gameId/design-content/:contentType/:scopeId/:contentId'
28
+ * ```
29
+ */
30
+ const gameParams = withObjectKeys(object({ gameId: string() }));
31
+ const itemParams = withObjectKeys(object({
32
+ gameId: string(),
33
+ contentType: string(),
34
+ scopeId: string(),
35
+ contentId: string(),
36
+ }));
37
+ /**
38
+ * `contentType` and `scopeId` narrow the listing; `includeData=true` opts into
39
+ * the payloads, which a picker does not need and a play session does.
40
+ */
41
+ const listQuery = object({
42
+ contentType: optional(string()),
43
+ scopeId: optional(string()),
44
+ includeData: optional(string()),
45
+ });
46
+ /** `cascade` is one of `none` (default), `prefix`, or `scope`. */
47
+ const deleteQuery = object({ cascade: optional(string()) });
48
+ /**
49
+ * Rows for a game, narrowed by type and scope.
50
+ *
51
+ * This one endpoint answers every listing story mode needs: the worlds to
52
+ * choose between (`contentType=world`), a world's whole design document
53
+ * (`scopeId=<worldId>&includeData=true`), the arcs offered for it
54
+ * (`contentType=story-arc&scopeId=<worldId>`), and the beats already written
55
+ * for an arc (`contentType=story-node&scopeId=<storyId>&includeData=true`) —
56
+ * which is the map of what is pregenerated and must not be generated again.
57
+ */
58
+ export const designContentListContract = defineApiContract({
59
+ method: "get",
60
+ requestPathParamsSchema: gameParams,
61
+ requestQuerySchema: listQuery,
62
+ pathResolver: ({ gameId }) => `/games/${gameId}/design-content`,
63
+ responsesByStatusCode: { 200: DesignContentListResponseSchema },
64
+ });
65
+ /** One row by key, or `item: undefined` when nothing is stored there. */
66
+ export const designContentLoadContract = defineApiContract({
67
+ method: "get",
68
+ requestPathParamsSchema: itemParams,
69
+ pathResolver: ({ gameId, contentType, scopeId, contentId }) => `/games/${gameId}/design-content/${contentType}/${scopeId}/${contentId}`,
70
+ responsesByStatusCode: { 200: DesignContentLoadResponseSchema },
71
+ });
72
+ /** Create or replace one row. Re-saving the same key keeps its `createdAt`. */
73
+ export const designContentSaveContract = defineApiContract({
74
+ method: "post",
75
+ requestPathParamsSchema: gameParams,
76
+ pathResolver: ({ gameId }) => `/games/${gameId}/design-content`,
77
+ requestBodySchema: DesignContentSaveRequestSchema,
78
+ responsesByStatusCode: { 200: DesignContentSaveResponseSchema },
79
+ });
80
+ /**
81
+ * Write many rows at once; `replace` makes it a snapshot rather than a merge,
82
+ * which is how a whole design document lands in one request.
83
+ */
84
+ export const designContentBulkSaveContract = defineApiContract({
85
+ method: "post",
86
+ requestPathParamsSchema: gameParams,
87
+ pathResolver: ({ gameId }) => `/games/${gameId}/design-content/bulk`,
88
+ requestBodySchema: DesignContentBulkSaveRequestSchema,
89
+ responsesByStatusCode: { 200: DesignContentBulkSaveResponseSchema },
90
+ });
91
+ /**
92
+ * Delete one row, and — depending on `?cascade=` — what hangs off it:
93
+ * `prefix` also removes the rows whose `contentId` sits under this one (a story
94
+ * branch and its descendants), `scope` also removes everything scoped to it,
95
+ * transitively (a world, its content, its arcs, and their beats).
96
+ */
97
+ export const designContentDeleteContract = defineApiContract({
98
+ method: "delete",
99
+ requestPathParamsSchema: itemParams,
100
+ requestQuerySchema: deleteQuery,
101
+ pathResolver: ({ gameId, contentType, scopeId, contentId }) => `/games/${gameId}/design-content/${contentType}/${scopeId}/${contentId}`,
102
+ responsesByStatusCode: { 200: DesignContentDeleteResponseSchema },
103
+ });
104
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"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"}