@onlyworlds/sdk 2.1.2 → 2.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/dist/index.js CHANGED
@@ -39,313 +39,6 @@ __export(index_exports, {
39
39
  });
40
40
  module.exports = __toCommonJS(index_exports);
41
41
 
42
- // src/token-resource.ts
43
- var TokenResource = class {
44
- constructor(client) {
45
- this.client = client;
46
- }
47
- /**
48
- * Get current token status for authenticated user
49
- *
50
- * Returns daily token allowance, usage, and availability.
51
- * Matches base-tool's checkStatus() pattern.
52
- *
53
- * @returns Current token status
54
- * @example
55
- * ```typescript
56
- * const status = await client.tokens.getStatus();
57
- * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
58
- * console.log(`Used today: ${status.tokens_used_today}`);
59
- * console.log(`Active sessions: ${status.sessions_active}`);
60
- * ```
61
- */
62
- async getStatus() {
63
- return this.client.request("GET", "/tokens/status/");
64
- }
65
- /**
66
- * Consume tokens for service usage
67
- *
68
- * Reports token consumption to track daily usage. Allows consumption even if
69
- * exceeds available tokens (tracks as debt), but warns via error field.
70
- * Matches base-tool's reportUsage() pattern.
71
- *
72
- * @param params - Token consumption parameters
73
- * @returns Consumption result with updated balance
74
- * @example
75
- * ```typescript
76
- * const result = await client.tokens.consume({
77
- * amount: 500,
78
- * service: 'worldbuilding_tool',
79
- * metadata: {
80
- * feature: 'character_generation',
81
- * model: 'gpt-4',
82
- * prompt_tokens: 300,
83
- * completion_tokens: 200
84
- * }
85
- * });
86
- *
87
- * if (result.error) {
88
- * console.warn('Token warning:', result.error);
89
- * }
90
- * console.log(`${result.tokens_remaining} tokens remaining`);
91
- * ```
92
- */
93
- async consume(params) {
94
- return this.client.request("POST", "/tokens/consume/", {
95
- body: {
96
- amount: params.amount,
97
- service: params.service || "sdk_client",
98
- session_id: params.sessionId ?? null,
99
- metadata: params.metadata ?? null
100
- }
101
- });
102
- }
103
- /**
104
- * Get encrypted OpenAI API key (advanced use case)
105
- *
106
- * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
107
- * Returns encrypted key that must be decrypted client-side using Fernet.
108
- *
109
- * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
110
- * including client-side decryption with the 'fernet' npm package.
111
- *
112
- * @returns Encrypted access key and session info
113
- * @throws Error if insufficient tokens (< 100)
114
- * @example
115
- * ```typescript
116
- * // Get encrypted key
117
- * const access = await client.tokens.getAccessKey();
118
- *
119
- * // Decrypt using fernet library (see base-tool for full example)
120
- * // 1. Derive key from world ID using SHA-256
121
- * // 2. Use 'fernet' npm package to decrypt
122
- * // 3. Use decrypted OpenAI key for direct API calls
123
- * // 4. Report usage with access.session_id
124
- *
125
- * console.log('Session:', access.session_id);
126
- * console.log('Expires:', access.expires_at);
127
- * ```
128
- */
129
- async getAccessKey() {
130
- return this.client.request("GET", "/tokens/access-key/");
131
- }
132
- /**
133
- * Revoke a specific token session
134
- *
135
- * Invalidates the session ID obtained from getAccessKey().
136
- * Use when cleaning up or on logout.
137
- *
138
- * @param sessionId - Session ID to revoke
139
- * @returns Revocation result
140
- * @example
141
- * ```typescript
142
- * await client.tokens.revokeSession('session-id-here');
143
- * ```
144
- */
145
- async revokeSession(sessionId) {
146
- return this.client.request(
147
- "POST",
148
- `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
149
- );
150
- }
151
- /**
152
- * Revoke all active sessions (emergency use)
153
- *
154
- * Invalidates all token sessions for the authenticated user.
155
- * Use for security cleanup or when sessions are stuck.
156
- *
157
- * @returns Revocation result with count of revoked sessions
158
- * @example
159
- * ```typescript
160
- * const result = await client.tokens.revokeAllSessions();
161
- * console.log(`Revoked ${result.sessions_revoked} sessions`);
162
- * ```
163
- */
164
- async revokeAllSessions() {
165
- return this.client.request("POST", "/tokens/revoke-all-sessions/");
166
- }
167
- /**
168
- * Get public encryption info (no auth required)
169
- *
170
- * Returns algorithm details and example code for client-side decryption.
171
- * Public endpoint - can be called without authentication.
172
- *
173
- * @returns Encryption algorithm and implementation details
174
- * @example
175
- * ```typescript
176
- * const info = await client.tokens.getEncryptionInfo();
177
- * console.log('Algorithm:', info.algorithm);
178
- * console.log('Key derivation:', info.key_derivation);
179
- * console.log(info.javascript_example);
180
- * ```
181
- */
182
- async getEncryptionInfo() {
183
- return this.client.request("GET", "/tokens/encryption-info/");
184
- }
185
- };
186
-
187
- // src/client.ts
188
- var Resource = class {
189
- constructor(client, elementType) {
190
- this.client = client;
191
- this.elementType = elementType;
192
- }
193
- async list(options) {
194
- const response = await this.client.request("GET", `/${this.elementType}/`, { params: options });
195
- if (Array.isArray(response)) {
196
- return {
197
- count: response.length,
198
- next: null,
199
- previous: null,
200
- results: response
201
- };
202
- }
203
- return response;
204
- }
205
- async get(id) {
206
- return this.client.request("GET", `/${this.elementType}/${id}/`);
207
- }
208
- async create(data) {
209
- const body = this.elementType === "pin" ? this.roundPinCoordinates(data) : data;
210
- return this.client.request("POST", `/${this.elementType}/`, { body });
211
- }
212
- async update(id, data) {
213
- const body = this.elementType === "pin" ? this.roundPinCoordinates(data) : data;
214
- return this.client.request("PATCH", `/${this.elementType}/${id}/`, { body });
215
- }
216
- async delete(id) {
217
- return this.client.request("DELETE", `/${this.elementType}/${id}/`);
218
- }
219
- /**
220
- * Round Pin coordinates to integers (API requirement)
221
- * @private
222
- */
223
- roundPinCoordinates(data) {
224
- const rounded = { ...data };
225
- if (typeof rounded.x === "number") rounded.x = Math.round(rounded.x);
226
- if (typeof rounded.y === "number") rounded.y = Math.round(rounded.y);
227
- if (typeof rounded.z === "number") rounded.z = Math.round(rounded.z);
228
- return rounded;
229
- }
230
- };
231
- var WorldResource = class {
232
- constructor(client) {
233
- this.client = client;
234
- }
235
- /**
236
- * Get the world associated with the current API key
237
- * Returns the world directly (not wrapped in pagination)
238
- */
239
- async get() {
240
- return this.client.request("GET", "/world/");
241
- }
242
- /**
243
- * Update the current world
244
- */
245
- async update(data) {
246
- return this.client.request("PATCH", "/world/", { body: data });
247
- }
248
- };
249
- var OnlyWorldsClient = class {
250
- constructor(config) {
251
- this.baseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
252
- this.headers = {
253
- "Content-Type": "application/json",
254
- "API-Key": config.apiKey,
255
- "API-Pin": config.apiPin
256
- };
257
- this.worlds = new WorldResource(this);
258
- this.tokens = new TokenResource(this);
259
- this.abilities = new Resource(this, "ability");
260
- this.characters = new Resource(this, "character");
261
- this.collectives = new Resource(this, "collective");
262
- this.constructs = new Resource(this, "construct");
263
- this.creatures = new Resource(this, "creature");
264
- this.events = new Resource(this, "event");
265
- this.families = new Resource(this, "family");
266
- this.institutions = new Resource(this, "institution");
267
- this.languages = new Resource(this, "language");
268
- this.laws = new Resource(this, "law");
269
- this.locations = new Resource(this, "location");
270
- this.maps = new Resource(this, "map");
271
- this.markers = new Resource(this, "marker");
272
- this.narratives = new Resource(this, "narrative");
273
- this.objects = new Resource(this, "object");
274
- this.phenomena = new Resource(this, "phenomenon");
275
- this.pins = new Resource(this, "pin");
276
- this.relations = new Resource(this, "relation");
277
- this.species = new Resource(this, "species");
278
- this.titles = new Resource(this, "title");
279
- this.traits = new Resource(this, "trait");
280
- this.zones = new Resource(this, "zone");
281
- }
282
- /**
283
- * Make a request to the OnlyWorlds API
284
- */
285
- async request(method, path, options) {
286
- const url = new URL(`${this.baseUrl}${path}`);
287
- if (options?.params) {
288
- Object.entries(options.params).forEach(([key, value]) => {
289
- if (value !== void 0 && value !== null) {
290
- url.searchParams.append(key, String(value));
291
- }
292
- });
293
- }
294
- const fetchOptions = {
295
- method,
296
- headers: this.headers
297
- };
298
- if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
299
- fetchOptions.body = JSON.stringify(options.body);
300
- }
301
- const response = await fetch(url.toString(), fetchOptions);
302
- if (!response.ok) {
303
- let errorMessage = `API Error ${response.status}`;
304
- try {
305
- const errorText = await response.text();
306
- if (errorText) {
307
- try {
308
- const errorJson = JSON.parse(errorText);
309
- if (Array.isArray(errorJson.detail)) {
310
- const validationErrors = errorJson.detail.map((err) => {
311
- const location = err.loc ? err.loc.join(".") : "unknown";
312
- return `${location}: ${err.msg}`;
313
- }).join("; ");
314
- errorMessage += `: ${validationErrors}`;
315
- } else {
316
- errorMessage += `: ${errorJson.detail || errorJson.error || errorText}`;
317
- }
318
- } catch {
319
- errorMessage += `: ${errorText}`;
320
- }
321
- }
322
- } catch {
323
- }
324
- throw new Error(errorMessage);
325
- }
326
- if (response.status === 204) {
327
- return void 0;
328
- }
329
- return response.json();
330
- }
331
- /**
332
- * Helper to convert nested objects to _id/_ids format
333
- */
334
- static prepareInput(data) {
335
- const result = { ...data };
336
- for (const [key, value] of Object.entries(result)) {
337
- if (value && typeof value === "object" && "id" in value) {
338
- delete result[key];
339
- result[`${key}_id`] = value.id;
340
- } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
341
- delete result[key];
342
- result[`${key}_ids`] = value.map((item) => item.id);
343
- }
344
- }
345
- return result;
346
- }
347
- };
348
-
349
42
  // src/types.ts
350
43
  var ElementType = /* @__PURE__ */ ((ElementType2) => {
351
44
  ElementType2["Ability"] = "ability";
@@ -1154,15 +847,368 @@ var FIELD_SCHEMA = {
1154
847
  principles: { type: "multi_link", target: "construct" }
1155
848
  }
1156
849
  };
1157
- function createElementId(id) {
1158
- return id;
1159
- }
1160
- function createElementIds(ids) {
1161
- return ids;
1162
- }
1163
- function createAnyElementId(id) {
1164
- return id;
1165
- }
850
+ function createElementId(id) {
851
+ return id;
852
+ }
853
+ function createElementIds(ids) {
854
+ return ids;
855
+ }
856
+ function createAnyElementId(id) {
857
+ return id;
858
+ }
859
+
860
+ // src/token-resource.ts
861
+ var TokenResource = class {
862
+ constructor(client) {
863
+ this.client = client;
864
+ }
865
+ /**
866
+ * Get current token status for authenticated user
867
+ *
868
+ * Returns daily token allowance, usage, and availability.
869
+ * Matches base-tool's checkStatus() pattern.
870
+ *
871
+ * @returns Current token status
872
+ * @example
873
+ * ```typescript
874
+ * const status = await client.tokens.getStatus();
875
+ * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
876
+ * console.log(`Used today: ${status.tokens_used_today}`);
877
+ * console.log(`Active sessions: ${status.sessions_active}`);
878
+ * ```
879
+ */
880
+ async getStatus() {
881
+ return this.client.request("GET", "/tokens/status/");
882
+ }
883
+ /**
884
+ * Consume tokens for service usage
885
+ *
886
+ * Reports token consumption to track daily usage. Allows consumption even if
887
+ * exceeds available tokens (tracks as debt), but warns via error field.
888
+ * Matches base-tool's reportUsage() pattern.
889
+ *
890
+ * @param params - Token consumption parameters
891
+ * @returns Consumption result with updated balance
892
+ * @example
893
+ * ```typescript
894
+ * const result = await client.tokens.consume({
895
+ * amount: 500,
896
+ * service: 'worldbuilding_tool',
897
+ * metadata: {
898
+ * feature: 'character_generation',
899
+ * model: 'gpt-4',
900
+ * prompt_tokens: 300,
901
+ * completion_tokens: 200
902
+ * }
903
+ * });
904
+ *
905
+ * if (result.error) {
906
+ * console.warn('Token warning:', result.error);
907
+ * }
908
+ * console.log(`${result.tokens_remaining} tokens remaining`);
909
+ * ```
910
+ */
911
+ async consume(params) {
912
+ return this.client.request("POST", "/tokens/consume/", {
913
+ body: {
914
+ amount: params.amount,
915
+ service: params.service || "sdk_client",
916
+ session_id: params.sessionId ?? null,
917
+ metadata: params.metadata ?? null
918
+ }
919
+ });
920
+ }
921
+ /**
922
+ * Get encrypted OpenAI API key (advanced use case)
923
+ *
924
+ * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
925
+ * Returns encrypted key that must be decrypted client-side using Fernet.
926
+ *
927
+ * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
928
+ * including client-side decryption with the 'fernet' npm package.
929
+ *
930
+ * @returns Encrypted access key and session info
931
+ * @throws Error if insufficient tokens (< 100)
932
+ * @example
933
+ * ```typescript
934
+ * // Get encrypted key
935
+ * const access = await client.tokens.getAccessKey();
936
+ *
937
+ * // Decrypt using fernet library (see base-tool for full example)
938
+ * // 1. Derive key from world ID using SHA-256
939
+ * // 2. Use 'fernet' npm package to decrypt
940
+ * // 3. Use decrypted OpenAI key for direct API calls
941
+ * // 4. Report usage with access.session_id
942
+ *
943
+ * console.log('Session:', access.session_id);
944
+ * console.log('Expires:', access.expires_at);
945
+ * ```
946
+ */
947
+ async getAccessKey() {
948
+ return this.client.request("GET", "/tokens/access-key/");
949
+ }
950
+ /**
951
+ * Revoke a specific token session
952
+ *
953
+ * Invalidates the session ID obtained from getAccessKey().
954
+ * Use when cleaning up or on logout.
955
+ *
956
+ * @param sessionId - Session ID to revoke
957
+ * @returns Revocation result
958
+ * @example
959
+ * ```typescript
960
+ * await client.tokens.revokeSession('session-id-here');
961
+ * ```
962
+ */
963
+ async revokeSession(sessionId) {
964
+ return this.client.request(
965
+ "POST",
966
+ `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
967
+ );
968
+ }
969
+ /**
970
+ * Revoke all active sessions (emergency use)
971
+ *
972
+ * Invalidates all token sessions for the authenticated user.
973
+ * Use for security cleanup or when sessions are stuck.
974
+ *
975
+ * @returns Revocation result with count of revoked sessions
976
+ * @example
977
+ * ```typescript
978
+ * const result = await client.tokens.revokeAllSessions();
979
+ * console.log(`Revoked ${result.sessions_revoked} sessions`);
980
+ * ```
981
+ */
982
+ async revokeAllSessions() {
983
+ return this.client.request("POST", "/tokens/revoke-all-sessions/");
984
+ }
985
+ /**
986
+ * Get public encryption info (no auth required)
987
+ *
988
+ * Returns algorithm details and example code for client-side decryption.
989
+ * Public endpoint - can be called without authentication.
990
+ *
991
+ * @returns Encryption algorithm and implementation details
992
+ * @example
993
+ * ```typescript
994
+ * const info = await client.tokens.getEncryptionInfo();
995
+ * console.log('Algorithm:', info.algorithm);
996
+ * console.log('Key derivation:', info.key_derivation);
997
+ * console.log(info.javascript_example);
998
+ * ```
999
+ */
1000
+ async getEncryptionInfo() {
1001
+ return this.client.request("GET", "/tokens/encryption-info/");
1002
+ }
1003
+ };
1004
+
1005
+ // src/client.ts
1006
+ var Resource = class {
1007
+ constructor(client, elementType) {
1008
+ this.client = client;
1009
+ this.elementType = elementType;
1010
+ }
1011
+ async list(options) {
1012
+ const response = await this.client.request("GET", `/${this.elementType}/`, { params: options });
1013
+ if (Array.isArray(response)) {
1014
+ return {
1015
+ count: response.length,
1016
+ next: null,
1017
+ previous: null,
1018
+ results: response
1019
+ };
1020
+ }
1021
+ return response;
1022
+ }
1023
+ async get(id) {
1024
+ return this.client.request("GET", `/${this.elementType}/${id}/`);
1025
+ }
1026
+ async create(data) {
1027
+ let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
1028
+ if (this.elementType === "pin") body = this.roundPinCoordinates(body);
1029
+ return this.client.request("POST", `/${this.elementType}/`, { body });
1030
+ }
1031
+ async update(id, data) {
1032
+ let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
1033
+ if (this.elementType === "pin") body = this.roundPinCoordinates(body);
1034
+ return this.client.request("PATCH", `/${this.elementType}/${id}/`, { body });
1035
+ }
1036
+ async delete(id) {
1037
+ return this.client.request("DELETE", `/${this.elementType}/${id}/`);
1038
+ }
1039
+ /**
1040
+ * Round Pin coordinates to integers (API requirement)
1041
+ * @private
1042
+ */
1043
+ roundPinCoordinates(data) {
1044
+ const rounded = { ...data };
1045
+ if (typeof rounded.x === "number") rounded.x = Math.round(rounded.x);
1046
+ if (typeof rounded.y === "number") rounded.y = Math.round(rounded.y);
1047
+ if (typeof rounded.z === "number") rounded.z = Math.round(rounded.z);
1048
+ return rounded;
1049
+ }
1050
+ };
1051
+ var WorldResource = class {
1052
+ constructor(client) {
1053
+ this.client = client;
1054
+ }
1055
+ /**
1056
+ * Get the world associated with the current API key
1057
+ * Returns the world directly (not wrapped in pagination)
1058
+ */
1059
+ async get() {
1060
+ return this.client.request("GET", "/world/");
1061
+ }
1062
+ /**
1063
+ * Update the current world
1064
+ */
1065
+ async update(data) {
1066
+ return this.client.request("PATCH", "/world/", { body: data });
1067
+ }
1068
+ };
1069
+ var OnlyWorldsClient = class {
1070
+ constructor(config) {
1071
+ this.baseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
1072
+ this.headers = {
1073
+ "Content-Type": "application/json",
1074
+ "API-Key": config.apiKey,
1075
+ "API-Pin": config.apiPin
1076
+ };
1077
+ this.worlds = new WorldResource(this);
1078
+ this.tokens = new TokenResource(this);
1079
+ this.abilities = new Resource(this, "ability");
1080
+ this.characters = new Resource(this, "character");
1081
+ this.collectives = new Resource(this, "collective");
1082
+ this.constructs = new Resource(this, "construct");
1083
+ this.creatures = new Resource(this, "creature");
1084
+ this.events = new Resource(this, "event");
1085
+ this.families = new Resource(this, "family");
1086
+ this.institutions = new Resource(this, "institution");
1087
+ this.languages = new Resource(this, "language");
1088
+ this.laws = new Resource(this, "law");
1089
+ this.locations = new Resource(this, "location");
1090
+ this.maps = new Resource(this, "map");
1091
+ this.markers = new Resource(this, "marker");
1092
+ this.narratives = new Resource(this, "narrative");
1093
+ this.objects = new Resource(this, "object");
1094
+ this.phenomena = new Resource(this, "phenomenon");
1095
+ this.pins = new Resource(this, "pin");
1096
+ this.relations = new Resource(this, "relation");
1097
+ this.species = new Resource(this, "species");
1098
+ this.titles = new Resource(this, "title");
1099
+ this.traits = new Resource(this, "trait");
1100
+ this.zones = new Resource(this, "zone");
1101
+ }
1102
+ /**
1103
+ * Make a request to the OnlyWorlds API
1104
+ */
1105
+ async request(method, path, options) {
1106
+ const url = new URL(`${this.baseUrl}${path}`);
1107
+ if (options?.params) {
1108
+ Object.entries(options.params).forEach(([key, value]) => {
1109
+ if (value !== void 0 && value !== null) {
1110
+ url.searchParams.append(key, String(value));
1111
+ }
1112
+ });
1113
+ }
1114
+ const fetchOptions = {
1115
+ method,
1116
+ headers: this.headers
1117
+ };
1118
+ if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
1119
+ fetchOptions.body = JSON.stringify(options.body);
1120
+ }
1121
+ const response = await fetch(url.toString(), fetchOptions);
1122
+ if (!response.ok) {
1123
+ let errorMessage = `API Error ${response.status}`;
1124
+ try {
1125
+ const errorText = await response.text();
1126
+ if (errorText) {
1127
+ try {
1128
+ const errorJson = JSON.parse(errorText);
1129
+ if (Array.isArray(errorJson.detail)) {
1130
+ const validationErrors = errorJson.detail.map((err) => {
1131
+ const location = err.loc ? err.loc.join(".") : "unknown";
1132
+ return `${location}: ${err.msg}`;
1133
+ }).join("; ");
1134
+ errorMessage += `: ${validationErrors}`;
1135
+ } else {
1136
+ errorMessage += `: ${errorJson.detail || errorJson.error || errorText}`;
1137
+ }
1138
+ } catch {
1139
+ errorMessage += `: ${errorText}`;
1140
+ }
1141
+ }
1142
+ } catch {
1143
+ }
1144
+ throw new Error(errorMessage);
1145
+ }
1146
+ if (response.status === 204) {
1147
+ return void 0;
1148
+ }
1149
+ return response.json();
1150
+ }
1151
+ /**
1152
+ * Helper to convert nested objects to _id/_ids format (legacy method)
1153
+ * @deprecated Use prepareRelations instead - it's called automatically in create/update
1154
+ */
1155
+ static prepareInput(data) {
1156
+ const result = { ...data };
1157
+ for (const [key, value] of Object.entries(result)) {
1158
+ if (value && typeof value === "object" && !Array.isArray(value) && "id" in value) {
1159
+ delete result[key];
1160
+ result[`${key}_id`] = value.id;
1161
+ } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1162
+ delete result[key];
1163
+ result[`${key}_ids`] = value.map((item) => item.id);
1164
+ }
1165
+ }
1166
+ return result;
1167
+ }
1168
+ /**
1169
+ * Convert relation fields to API format (_id/_ids suffix)
1170
+ *
1171
+ * The OnlyWorlds API expects:
1172
+ * - single_link fields: fieldname_id (e.g., birthplace_id)
1173
+ * - multi_link fields: fieldname_ids (e.g., species_ids)
1174
+ *
1175
+ * This method auto-converts based on FIELD_SCHEMA:
1176
+ * - { species: ["id1", "id2"] } → { species_ids: ["id1", "id2"] }
1177
+ * - { birthplace: "location-id" } → { birthplace_id: "location-id" }
1178
+ * - { species: [{id: "id1", name: "X"}] } → { species_ids: ["id1"] }
1179
+ *
1180
+ * Called automatically by create() and update() methods.
1181
+ */
1182
+ static prepareRelations(data, elementType) {
1183
+ const schema = FIELD_SCHEMA[elementType];
1184
+ if (!schema) return data;
1185
+ const result = { ...data };
1186
+ for (const [key, value] of Object.entries(result)) {
1187
+ const fieldDef = schema[key];
1188
+ if (!fieldDef) continue;
1189
+ if (fieldDef.type === "single_link") {
1190
+ delete result[key];
1191
+ if (value && typeof value === "object" && "id" in value) {
1192
+ result[`${key}_id`] = value.id;
1193
+ } else if (typeof value === "string" || value === null) {
1194
+ result[`${key}_id`] = value;
1195
+ }
1196
+ } else if (fieldDef.type === "multi_link") {
1197
+ delete result[key];
1198
+ if (Array.isArray(value)) {
1199
+ if (value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1200
+ result[`${key}_ids`] = value.map((item) => item.id);
1201
+ } else {
1202
+ result[`${key}_ids`] = value;
1203
+ }
1204
+ } else {
1205
+ result[`${key}_ids`] = [];
1206
+ }
1207
+ }
1208
+ }
1209
+ return result;
1210
+ }
1211
+ };
1166
1212
 
1167
1213
  // src/token-types.ts
1168
1214
  var GameTier = /* @__PURE__ */ ((GameTier2) => {