simple-strapi 1.0.0-alpha.26 → 1.0.0-alpha.28

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/README.md ADDED
@@ -0,0 +1,750 @@
1
+ # simple-strapi
2
+
3
+ > A lightweight, type-safe Strapi v5 client for Node.js and TypeScript. Define schemas once, get fully-typed responses and automatic populate queries — no manual configuration needed.
4
+
5
+ ## Features
6
+
7
+ - Schema-driven: define a schema and get fully-typed response data
8
+ - Automatic `populate` generation from schemas (deep, nested)
9
+ - Zod-based validation with safe parsing and warnings on mismatches
10
+ - Full CRUD: `getCollection`, `getSingle`, `create`, `update`, `delete`
11
+ - Auto-pagination: fetch all pages automatically or control pagination manually
12
+ - Authentication via API token or email/password credentials
13
+ - All fields support `required` option for strict TypeScript types
14
+ - Upload files to the Media Library
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install simple-strapi
20
+ # or
21
+ yarn add simple-strapi
22
+ # or
23
+ bun add simple-strapi
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import { StrapiClient } from "simple-strapi";
30
+
31
+ const client = await StrapiClient.create("https://my-strapi.example.com/api", {
32
+ auth: process.env.STRAPI_TOKEN, // API token (string)
33
+ });
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Client API
39
+
40
+ ### `StrapiClient.create(endpoint, options?)`
41
+
42
+ Creates and returns a new `StrapiClient` instance. This is the only way to instantiate the client.
43
+
44
+ ```ts
45
+ const client = await StrapiClient.create(endpoint, options?)
46
+ ```
47
+
48
+ | Parameter | Type | Description |
49
+ | ----------------- | ----------------------------------------------- | ---------------------------------------------------------- |
50
+ | `endpoint` | `string \| URL` | Full URL including base path (e.g. `https://host.com/api`) |
51
+ | `options.auth` | `string \| { email: string; password: string }` | API token string, or credentials object for JWT auth |
52
+ | `options.params` | `Record<string, any>` | Default query params for every request |
53
+ | `options.headers` | `Record<string, string>` | Default extra headers for every request |
54
+
55
+ **Examples:**
56
+
57
+ ```ts
58
+ // With API token
59
+ const client = await StrapiClient.create("https://host.com/api", {
60
+ auth: process.env.STRAPI_TOKEN,
61
+ });
62
+
63
+ // With email/password (fetches JWT automatically)
64
+ const client = await StrapiClient.create("https://host.com/api", {
65
+ auth: { email: "user@example.com", password: "secret" },
66
+ });
67
+
68
+ // Without auth (public API)
69
+ const client = await StrapiClient.create("https://host.com/api");
70
+ ```
71
+
72
+ ---
73
+
74
+ ### `client.getCollection(pluralId, options?)`
75
+
76
+ Fetches a collection of entries. By default fetches all pages automatically.
77
+
78
+ ```ts
79
+ const { data, meta } = await client.getCollection(pluralId, options?)
80
+ ```
81
+
82
+ | Parameter | Type | Default | Description |
83
+ | -------------------------- | ----------------------------------------------- | ------------- | ----------------------------------------------------------------- |
84
+ | `pluralId` | `string` | — | Strapi collection plural API ID (e.g. `"articles"`) |
85
+ | `options.schema` | `Schema` | — | Field schema for typed response and auto-populate |
86
+ | `options.pagination` | `false \| { page?: number; pageSize?: number }` | `{ page: 1 }` | `false` = fetch all pages; object = single page with given params |
87
+ | `options.sort` | `string \| string[]` | — | Sort expression(s) (e.g. `"createdAt:desc"`) |
88
+ | `options.filters` | `Record<string, any>` | — | Strapi filter object |
89
+ | `options.populate` | `any` | — | Manual populate (ignored if `schema` is provided) |
90
+ | `options.params` | `Record<string, any>` | — | Additional raw query params |
91
+ | `options.headers` | `Record<string, string>` | — | Request-specific extra headers |
92
+ | `options.where.documentId` | `string` | — | Append a documentId segment to the URL path |
93
+
94
+ **Returns:** `Promise<{ data: InferSchemaWithDefaults<S>[], meta: any }>`
95
+
96
+ **Examples:**
97
+
98
+ ```ts
99
+ import {
100
+ StrapiClient,
101
+ text,
102
+ number,
103
+ enumeration,
104
+ media,
105
+ component,
106
+ dynamic,
107
+ richText,
108
+ boolean,
109
+ } from "simple-strapi";
110
+
111
+ // Typed response with schema
112
+ const { data } = await client.getCollection("articles", {
113
+ pagination: false, // fetch all pages
114
+ sort: "publishedAt:desc",
115
+ filters: { status: { $eq: "published" } },
116
+ schema: {
117
+ title: text({ required: true }),
118
+ slug: text({ required: true }),
119
+ views: number(),
120
+ published: boolean(),
121
+ cover: media.single(),
122
+ category: enumeration(["news", "blog", "tutorial"]),
123
+ body: richText.blocks(),
124
+ },
125
+ });
126
+
127
+ // data is typed as Array<{ title: string; slug: string; views: number | null | undefined; ... }>
128
+
129
+ // Untyped (no schema)
130
+ const { data: raw } = await client.getCollection("articles");
131
+ ```
132
+
133
+ ---
134
+
135
+ ### `client.getSingle(pluralId, options?)`
136
+
137
+ Fetches a single entry. Works with both Strapi single types and collections (with filters to identify one entry).
138
+
139
+ ```ts
140
+ const { data, meta } = await client.getSingle(pluralId, options?)
141
+ ```
142
+
143
+ | Parameter | Type | Description |
144
+ | ------------------ | ------------------------ | ------------------------------------------------- |
145
+ | `pluralId` | `string` | Strapi collection or single type plural API ID |
146
+ | `options.schema` | `Schema` | Field schema for typed response and auto-populate |
147
+ | `options.populate` | `any` | Manual populate (ignored if `schema` is provided) |
148
+ | `options.params` | `Record<string, any>` | Query params, including `filters` |
149
+ | `options.headers` | `Record<string, string>` | Request-specific extra headers |
150
+
151
+ **Returns:** `Promise<{ data: InferSchemaWithDefaults<S>; meta: any }>`
152
+
153
+ **Example:**
154
+
155
+ ```ts
156
+ const { data } = await client.getSingle("homepage", {
157
+ schema: {
158
+ title: text({ required: true }),
159
+ subtitle: text(),
160
+ hero: media.single({ required: true }),
161
+ },
162
+ });
163
+
164
+ // With filters to identify one entry from a collection
165
+ const { data: article } = await client.getSingle("articles", {
166
+ params: { filters: { slug: { $eq: "my-article" } } },
167
+ schema: { title: text({ required: true }), body: richText.blocks() },
168
+ });
169
+ ```
170
+
171
+ ---
172
+
173
+ ### `client.update(pluralId, documentId, payload, options?)`
174
+
175
+ Updates an existing entry by `documentId`.
176
+
177
+ ```ts
178
+ const { data, meta } = await client.update(pluralId, documentId, payload, options?)
179
+ ```
180
+
181
+ | Parameter | Type | Description |
182
+ | ----------------- | ------------------------ | ------------------------------------------------------- |
183
+ | `pluralId` | `string` | Strapi collection plural API ID |
184
+ | `documentId` | `string` | The Strapi v5 document ID |
185
+ | `payload` | `any` | Data to update (will be wrapped in `{ data: payload }`) |
186
+ | `options.schema` | `Schema` | Schema for parsing the response |
187
+ | `options.params` | `Record<string, any>` | Additional query params |
188
+ | `options.headers` | `Record<string, string>` | Request-specific extra headers |
189
+
190
+ **Returns:** `Promise<{ data: InferSchemaWithDefaults<S>; meta: any }>`
191
+
192
+ **Example:**
193
+
194
+ ```ts
195
+ const { data } = await client.update(
196
+ "articles",
197
+ "abc123",
198
+ { title: "New Title" },
199
+ {
200
+ schema: { title: text({ required: true }) },
201
+ },
202
+ );
203
+ ```
204
+
205
+ ---
206
+
207
+ ### `client.create(pluralId, payload, options?)`
208
+
209
+ Creates a new entry in the collection.
210
+
211
+ ```ts
212
+ const { data, meta } = await client.create(pluralId, payload, options?)
213
+ ```
214
+
215
+ | Parameter | Type | Description |
216
+ | ----------------- | ------------------------ | --------------------------------------------------------------- |
217
+ | `pluralId` | `string` | Strapi collection plural API ID |
218
+ | `payload` | `any` | Data for the new entry (will be wrapped in `{ data: payload }`) |
219
+ | `options.schema` | `Schema` | Schema for parsing the response |
220
+ | `options.params` | `Record<string, any>` | Additional query params |
221
+ | `options.headers` | `Record<string, string>` | Request-specific extra headers |
222
+
223
+ **Returns:** `Promise<{ data: InferSchemaWithDefaults<S>; meta: any }>`
224
+
225
+ **Example:**
226
+
227
+ ```ts
228
+ const { data } = await client.create("articles", { title: "Hello World", slug: "hello-world" });
229
+ ```
230
+
231
+ ---
232
+
233
+ ### `client.delete(pluralId, documentId, options?)`
234
+
235
+ Deletes an entry by `documentId`.
236
+
237
+ ```ts
238
+ const { data, meta } = await client.delete(pluralId, documentId, options?)
239
+ ```
240
+
241
+ | Parameter | Type | Description |
242
+ | ----------------- | ------------------------ | ------------------------------- |
243
+ | `pluralId` | `string` | Strapi collection plural API ID |
244
+ | `documentId` | `string` | The Strapi v5 document ID |
245
+ | `options.params` | `Record<string, any>` | Additional query params |
246
+ | `options.headers` | `Record<string, string>` | Request-specific extra headers |
247
+
248
+ **Returns:** `Promise<{ data: { documentId: string }; meta: {} }>` (on 204) or `Promise<{ data: any; meta: any }>`
249
+
250
+ **Example:**
251
+
252
+ ```ts
253
+ await client.delete("articles", "abc123");
254
+ ```
255
+
256
+ ---
257
+
258
+ ### `client.upload(file, options?)`
259
+
260
+ Uploads a file to the Strapi Media Library. Returns an array of the uploaded media objects.
261
+
262
+ ```ts
263
+ const media = await client.upload(file, options?)
264
+ ```
265
+
266
+ | Parameter | Type | Description |
267
+ | ------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
268
+ | `file` | `Blob \| File \| string` | File source: a `Blob`, a browser `File`, a base64 data URI (`data:mime;base64,...`), or a raw base64 string |
269
+ | `options.filename` | `string` | File name in the upload form. Required for `Blob` and raw base64; auto-extracted from `File` |
270
+ | `options.ref` | `string` | Content Type name (e.g. `"product"` → auto-resolves to `api::product.product`) or a full UID (e.g. `"plugin::users-permissions.user"`) |
271
+ | `options.refId` | `string \| number` | `documentId` of the entity to attach the file to |
272
+ | `options.field` | `string` | Top-level field name on the entity. Nested fields (dot-notation) are not supported by Strapi's `/upload` endpoint |
273
+ | `options.headers` | `Record<string, string>` | Extra request headers |
274
+
275
+ **Returns:** `Promise<ZodMediaType[]>`
276
+
277
+ **Examples:**
278
+
279
+ ```ts
280
+ // Upload a File from a browser input
281
+ const [file] = inputEl.files!;
282
+ const [uploaded] = await client.upload(file);
283
+ console.log(uploaded.url);
284
+
285
+ // Upload a base64 data URI and attach it to an entity
286
+ const [media] = await client.upload("data:image/png;base64,iVBORw0KGgo...", {
287
+ filename: "cover.png",
288
+ ref: "article", // auto-resolved to api::article.article
289
+ refId: "abc123",
290
+ field: "cover",
291
+ });
292
+
293
+ // Upload a Blob with a custom filename
294
+ const blob = new Blob([buffer], { type: "image/jpeg" });
295
+ const [result] = await client.upload(blob, { filename: "photo.jpg" });
296
+ ```
297
+
298
+ > **Note:** Strapi's `/upload` endpoint does not support attaching files to nested fields via dot-notation. Upload the file first, then use `client.update()` to set the field.
299
+
300
+ ---
301
+
302
+ ## Schema field helpers
303
+
304
+ Schemas are plain objects where each key maps to a field definition tuple. All field helpers accept an optional `options` object.
305
+
306
+ > **Default nullability**: all fields default to `T | null | undefined` unless `{ required: true }` is passed. This reflects Strapi's real-world behavior where fields are frequently optional.
307
+
308
+ ---
309
+
310
+ ### `text(options?)`
311
+
312
+ A string field.
313
+
314
+ ```ts
315
+ import { text } from "simple-strapi";
316
+
317
+ text(); // string | null | undefined
318
+ text({ required: true }); // string
319
+ ```
320
+
321
+ | Option | Type | Default | Description |
322
+ | ---------- | --------- | ------- | ------------------------------------------------------------------ |
323
+ | `required` | `boolean` | `false` | If true, type is `string` instead of `string \| null \| undefined` |
324
+
325
+ TypeScript types: `TextField`, `TextOptions`, `InferText<O>`
326
+
327
+ ---
328
+
329
+ ### `number(options?)`
330
+
331
+ A numeric field.
332
+
333
+ ```ts
334
+ import { number } from "simple-strapi";
335
+
336
+ number(); // number | null | undefined
337
+ number({ required: true }); // number
338
+ ```
339
+
340
+ | Option | Type | Default | Description |
341
+ | ---------- | --------- | ------- | ------------------------------------------------------------------ |
342
+ | `required` | `boolean` | `false` | If true, type is `number` instead of `number \| null \| undefined` |
343
+
344
+ TypeScript types: `NumberField`, `NumberOptions`, `InferNumber<O>`
345
+
346
+ ---
347
+
348
+ ### `boolean(options?)`
349
+
350
+ A boolean field.
351
+
352
+ ```ts
353
+ import { boolean } from "simple-strapi";
354
+
355
+ boolean(); // boolean | null | undefined
356
+ boolean({ required: true }); // boolean
357
+ ```
358
+
359
+ | Option | Type | Default | Description |
360
+ | ---------- | --------- | ------- | -------------------------------------------------------------------- |
361
+ | `required` | `boolean` | `false` | If true, type is `boolean` instead of `boolean \| null \| undefined` |
362
+
363
+ TypeScript types: `BooleanField`, `BooleanOptions`, `InferBoolean<O>`
364
+
365
+ ---
366
+
367
+ ### `json(options?)`
368
+
369
+ A JSON field (typed as `any`).
370
+
371
+ ```ts
372
+ import { json } from "simple-strapi";
373
+
374
+ json(); // any | null | undefined
375
+ json({ required: true }); // any
376
+ ```
377
+
378
+ | Option | Type | Default | Description |
379
+ | ---------- | --------- | ------- | ------------------------------------------------------------ |
380
+ | `required` | `boolean` | `false` | If true, type is `any` instead of `any \| null \| undefined` |
381
+
382
+ TypeScript types: `JSONField`, `JSONOptions`, `InferJSON<O>`
383
+
384
+ ---
385
+
386
+ ### `enumeration(values, options?)`
387
+
388
+ An enum field constrained to a fixed list of string values.
389
+
390
+ ```ts
391
+ import { enumeration } from "simple-strapi";
392
+
393
+ enumeration(["draft", "published", "archived"]);
394
+ // "draft" | "published" | "archived" | null | undefined
395
+
396
+ enumeration(["draft", "published"], { required: true });
397
+ // "draft" | "published"
398
+ ```
399
+
400
+ | Parameter | Type | Description |
401
+ | --------- | -------------------------------- | ---------------------------------------- |
402
+ | `values` | `readonly [string, ...string[]]` | Non-empty tuple of allowed string values |
403
+
404
+ | Option | Type | Default | Description |
405
+ | ---------- | --------- | ------- | ------------------------------------------------------------------------ |
406
+ | `required` | `boolean` | `false` | If true, type is `V[number]` instead of `V[number] \| null \| undefined` |
407
+
408
+ TypeScript types: `EnumerationField`, `EnumerationOptions`, `InferEnumeration<V, O>`
409
+
410
+ ---
411
+
412
+ ### `media.single(options?)`
413
+
414
+ A single Strapi media upload field. Automatically adds the correct `populate` entry.
415
+
416
+ ```ts
417
+ import { media } from "simple-strapi";
418
+
419
+ media.single(); // MediaType | null | undefined
420
+ media.single({ required: true }); // MediaType
421
+ ```
422
+
423
+ The resolved `MediaType` shape:
424
+
425
+ ```ts
426
+ {
427
+ id: number;
428
+ name: string;
429
+ alternativeText: string | null;
430
+ caption: string | null;
431
+ width: number | null;
432
+ height: number | null;
433
+ formats: Record<string, { name; hash?; ext?; mime; path?; size; url; width; height }> |
434
+ null |
435
+ undefined;
436
+ hash: string;
437
+ ext: string;
438
+ mime: string;
439
+ size: number;
440
+ url: string;
441
+ previewUrl: string | null;
442
+ provider: string;
443
+ provider_metadata: unknown | null;
444
+ createdAt: string;
445
+ updatedAt: string;
446
+ }
447
+ ```
448
+
449
+ | Option | Type | Default | Description |
450
+ | ---------- | --------- | ------- | ------------------------------------------------------------------------ |
451
+ | `required` | `boolean` | `false` | If true, type is `MediaType` instead of `MediaType \| null \| undefined` |
452
+
453
+ TypeScript types: `MediaSingleField`, `MediaSingleOptions`, `InferMediaSingle<O>`, `ZodMediaType`, `zodMediaSchema`
454
+
455
+ ---
456
+
457
+ ### `media.multiple(options?)`
458
+
459
+ A multiple Strapi media upload field (array). Automatically adds the correct `populate` entry.
460
+
461
+ ```ts
462
+ import { media } from "simple-strapi";
463
+
464
+ media.multiple(); // MediaType[] | null | undefined
465
+ media.multiple({ required: true }); // MediaType[]
466
+ ```
467
+
468
+ The resolved `MediaType` shape is the same as [`media.single()`](#mediasingleoptions).
469
+
470
+ | Option | Type | Default | Description |
471
+ | ---------- | --------- | ------- | -------------------------------------------------------------------------------- |
472
+ | `required` | `boolean` | `false` | If true, type is `MediaType[]` instead of `MediaType[] \| null \| undefined` |
473
+
474
+ TypeScript types: `MediaMultipleField`, `MediaMultipleOptions`, `InferMediaMultiple<O>`
475
+
476
+ ---
477
+
478
+ ### `richText.blocks(options?)`
479
+
480
+ A Strapi rich text blocks field (Strapi v5 block editor format).
481
+
482
+ ```ts
483
+ import { richText } from "simple-strapi";
484
+
485
+ richText.blocks(); // RichTextBlocks | null | undefined
486
+ richText.blocks({ required: true }); // RichTextBlocks
487
+ ```
488
+
489
+ `RichTextBlocks` is an array of block nodes:
490
+
491
+ ```ts
492
+ type RichTextBlocks = Array<
493
+ | { type: "paragraph"; children: ParagraphChild[] }
494
+ | { type: "heading"; level: number; children: ParagraphChild[] }
495
+ | { type: "list"; format: "ordered" | "unordered"; children: ListItemBlock[] }
496
+ >;
497
+
498
+ type ListItemBlock = {
499
+ type: "list-item";
500
+ children: ParagraphChild[];
501
+ };
502
+
503
+ type ParagraphChild =
504
+ | {
505
+ type: "text";
506
+ text: string;
507
+ bold?: boolean;
508
+ italic?: boolean;
509
+ underline?: boolean;
510
+ strikethrough?: boolean;
511
+ code?: boolean;
512
+ }
513
+ | { type: "link"; url: string; children: ParagraphChild[] };
514
+ ```
515
+
516
+ | Option | Type | Default | Description |
517
+ | ---------- | --------- | ------- | ---------------------------------------------------------------------------------- |
518
+ | `required` | `boolean` | `false` | If true, type is `RichTextBlocks` instead of `RichTextBlocks \| null \| undefined` |
519
+
520
+ TypeScript types: `RichTextBlocksField`, `RichTextBlocksOptions`, `InferRichTextBlocks<O>`, `RichTextBlocks`, `ParagraphChild`, `zodRichTextBlocksSchema`, `paragraphChild`
521
+
522
+ ---
523
+
524
+ ### `component.single(schema, options?)`
525
+
526
+ A Strapi single (non-repeatable) component. Automatically populates nested fields.
527
+
528
+ ```ts
529
+ import { component, text, media } from "simple-strapi";
530
+
531
+ component.single({ title: text(), image: media.single() });
532
+ // { id: number; documentId?: string; title: string | null | undefined; image: MediaType | null | undefined; ... } | null | undefined
533
+
534
+ component.single({ title: text() }, { required: true });
535
+ // { id: number; ...; title: string | null | undefined } — not nullable
536
+ ```
537
+
538
+ | Parameter | Type | Description |
539
+ | --------- | -------- | ----------------------------- |
540
+ | `schema` | `Schema` | Field schema of the component |
541
+
542
+ | Option | Type | Default | Description |
543
+ | ---------- | --------- | ------- | ------------------------------------------- |
544
+ | `required` | `boolean` | `false` | If true, component is not nullable/optional |
545
+
546
+ TypeScript types: `ComponentSingleField`, `ComponentSingleOptions`, `InferComponentSingle<S, O>`
547
+
548
+ ---
549
+
550
+ ### `component.repeatable(schema, options?)`
551
+
552
+ A Strapi repeatable component (array). Automatically populates nested fields.
553
+
554
+ ```ts
555
+ import { component, text } from "simple-strapi";
556
+
557
+ component.repeatable({ label: text(), value: text() });
558
+ // Array<{ id: number; label: string | null | undefined; ... }> | null | undefined
559
+
560
+ component.repeatable({ label: text() }, { required: true });
561
+ // Array<{ id: number; label: string | null | undefined; ... }>
562
+ ```
563
+
564
+ | Parameter | Type | Description |
565
+ | --------- | -------- | ----------------------------------- |
566
+ | `schema` | `Schema` | Field schema of the component items |
567
+
568
+ | Option | Type | Default | Description |
569
+ | ---------- | --------- | ------- | --------------------------------------- |
570
+ | `required` | `boolean` | `false` | If true, array is not nullable/optional |
571
+
572
+ TypeScript types: `ComponentRepeatableField`, `ComponentRepeatableOptions`, `InferComponentRepeatable<S, O>`
573
+
574
+ ---
575
+
576
+ ### `relation.hasMany(schema, options?)`
577
+
578
+ A Strapi relation that returns multiple related entries. Automatically populates the relation.
579
+
580
+ ```ts
581
+ import { relation, text } from "simple-strapi";
582
+
583
+ relation.hasMany({ name: text() });
584
+ // Array<{ id: number; name: string | null | undefined; ... }>
585
+
586
+ relation.hasMany({ name: text() }, { nullable: true, optional: true });
587
+ // Array<...> | null | undefined
588
+ ```
589
+
590
+ | Parameter | Type | Description |
591
+ | --------- | -------- | ----------------------------------- |
592
+ | `schema` | `Schema` | Field schema of the related entries |
593
+
594
+ | Option | Type | Default | Description |
595
+ | ---------- | --------- | ------- | ---------------------------------- |
596
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
597
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
598
+
599
+ TypeScript types: `RelationHasManyField`, `RelationHasManyOptions`, `InferRelationHasMany<S, O>`
600
+
601
+ ---
602
+
603
+ ### `relation.hasOne(schema, options?)`
604
+
605
+ A Strapi relation that returns a single related entry. Automatically populates the relation.
606
+
607
+ ```ts
608
+ import { relation, text } from "simple-strapi";
609
+
610
+ relation.hasOne({ name: text() });
611
+ // { id: number; name: string | null | undefined; ... }
612
+
613
+ relation.hasOne({ name: text() }, { nullable: true });
614
+ // { id: number; ... } | null
615
+ ```
616
+
617
+ | Parameter | Type | Description |
618
+ | --------- | -------- | --------------------------------- |
619
+ | `schema` | `Schema` | Field schema of the related entry |
620
+
621
+ | Option | Type | Default | Description |
622
+ | ---------- | --------- | ------- | ---------------------------------- |
623
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
624
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
625
+
626
+ TypeScript types: `RelationHasOneField`, `RelationHasOneOptions`, `InferRelationHasOne<S, O>`
627
+
628
+ ---
629
+
630
+ ### `dynamic(blocks, options?)`
631
+
632
+ A Strapi dynamic zone. Each key is a component UID, each value is the component's schema. The result is a discriminated union array tagged with `__component`.
633
+
634
+ ```ts
635
+ import { dynamic, text, media, enumeration, component, richText } from "simple-strapi";
636
+
637
+ dynamic({
638
+ "blocks.hero": { title: text({ required: true }), image: media.single() },
639
+ "blocks.content": { body: richText.blocks() },
640
+ });
641
+ // Array<
642
+ // | { __component: "blocks.hero"; title: string; image: MediaType | null | undefined }
643
+ // | { __component: "blocks.content"; body: RichTextBlocks | null | undefined }
644
+ // >
645
+ ```
646
+
647
+ | Parameter | Type | Description |
648
+ | --------- | ------------------------ | -------------------------------------- |
649
+ | `blocks` | `Record<string, Schema>` | Map of component UIDs to their schemas |
650
+
651
+ | Option | Type | Default | Description |
652
+ | ---------- | --------- | ------- | ---------------------------------- |
653
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
654
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
655
+
656
+ TypeScript types: `DynamicField`, `DynamicOptions`, `InferDynamic<B, O>`
657
+
658
+ ---
659
+
660
+ ## TypeScript type utilities
661
+
662
+ All types exported from `simple-strapi`:
663
+
664
+ | Type | Description |
665
+ | ---------------------------- | ----------------------------------------------------------------------------------------- |
666
+ | `Schema` | `Record<string, SchemaField>` — the shape of a schema definition |
667
+ | `SchemaField` | Union of all field tuple types |
668
+ | `InferSchema<S>` | Infers the TypeScript shape from a `Schema` (without default Strapi fields) |
669
+ | `InferSchemaWithDefaults<S>` | Same as `InferSchema<S>` plus `id`, `documentId`, `createdAt`, `updatedAt`, `publishedAt` |
670
+
671
+ ---
672
+
673
+ ## Default Strapi fields
674
+
675
+ Every entity returned by the client automatically includes these fields (regardless of schema):
676
+
677
+ | Field | Type |
678
+ | ------------- | -------------------------------------------- |
679
+ | `id` | `number` |
680
+ | `documentId` | `string \| undefined` |
681
+ | `createdAt` | `string \| undefined` (ISO datetime) |
682
+ | `updatedAt` | `string \| undefined` (ISO datetime) |
683
+ | `publishedAt` | `string \| null \| undefined` (ISO datetime) |
684
+
685
+ ---
686
+
687
+ ## Full example
688
+
689
+ ```ts
690
+ import {
691
+ StrapiClient,
692
+ text,
693
+ number,
694
+ boolean,
695
+ json,
696
+ enumeration,
697
+ media,
698
+ richText,
699
+ component,
700
+ dynamic,
701
+ relation,
702
+ } from "simple-strapi";
703
+
704
+ const client = await StrapiClient.create(process.env.STRAPI_URL!, {
705
+ auth: process.env.STRAPI_TOKEN,
706
+ });
707
+
708
+ const { data: events } = await client.getCollection("events", {
709
+ pagination: false,
710
+ sort: "publishedAt:desc",
711
+ filters: { status: { $eq: "published" } },
712
+ schema: {
713
+ title: text({ required: true }),
714
+ slug: text({ required: true }),
715
+ status: enumeration(["draft", "published", "archived"], { required: true }),
716
+ featured: boolean(),
717
+ price: number(),
718
+ metadata: json(),
719
+ cover: media.single({ required: true }),
720
+ tags: relation.hasMany({ name: text({ required: true }) }),
721
+ location: relation.hasOne({ city: text(), country: text() }),
722
+ header: component.single({
723
+ headline: text({ required: true }),
724
+ background: media.single(),
725
+ }),
726
+ speakers: component.repeatable({
727
+ name: text({ required: true }),
728
+ bio: richText.blocks(),
729
+ avatar: media.single(),
730
+ }),
731
+ blocks: dynamic({
732
+ "blocks.text": { body: richText.blocks({ required: true }) },
733
+ "blocks.gallery": {
734
+ layout: enumeration(["grid", "masonry"]),
735
+ images: component.repeatable({ image: media.single({ required: true }), caption: text() }),
736
+ },
737
+ }),
738
+ },
739
+ });
740
+ ```
741
+
742
+ ---
743
+
744
+ ## License
745
+
746
+ [ISC](../LICENSE)
747
+
748
+ ## Maintainer
749
+
750
+ [@hund-ernesto](https://github.com/hund-ernesto)
package/dist/client.d.ts CHANGED
@@ -6,7 +6,7 @@ import z from "zod";
6
6
  import { DynamicField, DynamicOptions, InferDynamic } from "./fields/dynamic";
7
7
  import { defaultStrapiFieldsSchema } from "./utils/schema";
8
8
  import { ComponentRepeatableField, ComponentRepeatableOptions, ComponentSingleField, ComponentSingleOptions, InferComponentRepeatable, InferComponentSingle } from "./fields/component";
9
- import { InferMediaSingle, MediaSingleField, MediaSingleOptions, ZodMediaType } from "./fields/media";
9
+ import { InferMediaSingle, MediaSingleField, MediaSingleOptions, InferMediaMultiple, MediaMultipleField, MediaMultipleOptions, ZodMediaType } from "./fields/media";
10
10
  import { EnumerationField, EnumerationOptions, InferEnumeration } from "./fields/enumeration";
11
11
  import { InferRichTextBlocks, RichTextBlocksField, RichTextBlocksOptions } from "./fields/richText";
12
12
  import { InferJSON, JSONField, JSONOptions } from "./fields/json";
@@ -16,7 +16,7 @@ type EntityRequest<P = {}> = {
16
16
  params?: RequestParams;
17
17
  headers?: Record<string, string>;
18
18
  } & P;
19
- export type SchemaField = TextField | NumberField | BooleanField | RelationHasManyField | RelationHasOneField | DynamicField | ComponentSingleField | ComponentRepeatableField | MediaSingleField | EnumerationField | RichTextBlocksField | JSONField;
19
+ export type SchemaField = TextField | NumberField | BooleanField | RelationHasManyField | RelationHasOneField | DynamicField | ComponentSingleField | ComponentRepeatableField | MediaSingleField | MediaMultipleField | EnumerationField | RichTextBlocksField | JSONField;
20
20
  export type Schema = Record<string, SchemaField>;
21
21
  export type InferSchema<S extends Schema> = {
22
22
  [K in keyof S]: S[K] extends ["text", infer O extends TextOptions] ? InferText<O> : S[K] extends ["number", infer O extends NumberOptions] ? InferNumber<O> : S[K] extends ["boolean", infer O extends BooleanOptions] ? InferBoolean<O> : S[K] extends ["json", infer O extends JSONOptions] ? InferJSON<O> : S[K] extends [
@@ -39,7 +39,7 @@ export type InferSchema<S extends Schema> = {
39
39
  "dynamic",
40
40
  infer B extends Record<string, Schema>,
41
41
  infer O extends DynamicOptions
42
- ] ? InferDynamic<B, O> : S[K] extends ["media.single", infer O extends MediaSingleOptions] ? InferMediaSingle<O> : S[K] extends [
42
+ ] ? InferDynamic<B, O> : S[K] extends ["media.single", infer O extends MediaSingleOptions] ? InferMediaSingle<O> : S[K] extends ["media.multiple", infer O extends MediaMultipleOptions] ? InferMediaMultiple<O> : S[K] extends [
43
43
  "enumeration",
44
44
  infer V extends readonly [string, ...string[]],
45
45
  infer O extends EnumerationOptions
package/dist/client.js CHANGED
@@ -1,7 +1,20 @@
1
1
  import { createSimpleException, ensureSimpleException } from "simple-exception";
2
2
  import { join } from "path";
3
3
  import fetch from "node-fetch";
4
+ import http from "http";
5
+ import https from "https";
4
6
  import qs from "qs";
7
+ const httpAgent = new http.Agent({ keepAlive: true });
8
+ const httpsAgent = new https.Agent({ keepAlive: true });
9
+ function agentFor(url) {
10
+ return url.protocol === "https:" ? httpsAgent : httpAgent;
11
+ }
12
+ async function safeResponseJson(response) {
13
+ const text = await response.text();
14
+ if (!text)
15
+ return null;
16
+ return JSON.parse(text);
17
+ }
5
18
  import z from "zod";
6
19
  import { defaultStrapiFields, schemaToParser } from "./utils/schema";
7
20
  import { zodMediaSchema, } from "./fields/media";
@@ -37,6 +50,7 @@ class Client {
37
50
  method: "POST",
38
51
  headers: this.headers,
39
52
  body: JSON.stringify({ identifier: auth.email, password: auth.password }),
53
+ agent: agentFor(requestURL),
40
54
  });
41
55
  if (!response.ok) {
42
56
  throw createSimpleException({
@@ -46,7 +60,7 @@ class Client {
46
60
  source: "strapi-utils/client.ts",
47
61
  });
48
62
  }
49
- const data = await response.json();
63
+ const data = await safeResponseJson(response);
50
64
  const { token } = z.object({ token: z.string() }).parse(data);
51
65
  return token;
52
66
  }
@@ -111,6 +125,7 @@ class Client {
111
125
  populate[key] = true;
112
126
  break;
113
127
  case "media.single":
128
+ case "media.multiple":
114
129
  populate[key] = { populate: true };
115
130
  break;
116
131
  case "dynamic":
@@ -166,6 +181,7 @@ class Client {
166
181
  ...this.getAuthorizedHeaders(),
167
182
  ...headers,
168
183
  },
184
+ agent: agentFor(requestURL),
169
185
  });
170
186
  if (!response.ok) {
171
187
  throw createSimpleException({
@@ -177,7 +193,7 @@ class Client {
177
193
  }
178
194
  const { data, meta } = z
179
195
  .object({ data: z.any(), meta: z.any() })
180
- .parse(await response.json());
196
+ .parse(await safeResponseJson(response));
181
197
  if (!data)
182
198
  throw createSimpleException({ code: 404, type: "error", message: "Not found" });
183
199
  if ("schema" in options) {
@@ -233,6 +249,7 @@ class Client {
233
249
  ...this.getAuthorizedHeaders(),
234
250
  ...headers,
235
251
  },
252
+ agent: agentFor(requestURL),
236
253
  });
237
254
  if (!response.ok) {
238
255
  throw createSimpleException({
@@ -242,7 +259,7 @@ class Client {
242
259
  source: "strapi-utils/client.ts",
243
260
  });
244
261
  }
245
- const responseData = await response.json();
262
+ const responseData = await safeResponseJson(response);
246
263
  const { data, meta } = z
247
264
  // .object({ data: z.array(z.any()).catch([]), meta: z.any() })
248
265
  .object({ data: z.any(), meta: z.any() })
@@ -309,6 +326,7 @@ class Client {
309
326
  ...headers,
310
327
  },
311
328
  body: JSON.stringify({ data: payload }),
329
+ agent: agentFor(requestURL),
312
330
  });
313
331
  if (!response.ok) {
314
332
  const errorBody = await response.json().catch(() => ({}));
@@ -327,7 +345,7 @@ class Client {
327
345
  }
328
346
  const { data, meta } = z
329
347
  .object({ data: z.any(), meta: z.any() })
330
- .parse(await response.json());
348
+ .parse(await safeResponseJson(response));
331
349
  if ("schema" in options && options.schema) {
332
350
  const shape = options.schema;
333
351
  const schema = z.object(schemaToParser(shape)).extend(defaultStrapiFields).loose();
@@ -362,6 +380,7 @@ class Client {
362
380
  ...this.getAuthorizedHeaders(),
363
381
  ...headers,
364
382
  },
383
+ agent: agentFor(requestURL),
365
384
  });
366
385
  if (!response.ok) {
367
386
  const errorBody = await response.json().catch(() => ({}));
@@ -377,7 +396,7 @@ class Client {
377
396
  }
378
397
  const { data, meta } = z
379
398
  .object({ data: z.any(), meta: z.any() })
380
- .parse(await response.json());
399
+ .parse(await safeResponseJson(response));
381
400
  return { data, meta };
382
401
  }
383
402
  catch (exception) {
@@ -456,6 +475,7 @@ class Client {
456
475
  ...headers,
457
476
  },
458
477
  body: formData,
478
+ agent: agentFor(requestURL),
459
479
  });
460
480
  if (!response.ok) {
461
481
  const errorBody = await response.json().catch(() => ({}));
@@ -466,7 +486,7 @@ class Client {
466
486
  source: "strapi-utils/client.ts",
467
487
  });
468
488
  }
469
- const data = await response.json();
489
+ const data = await safeResponseJson(response);
470
490
  return z.array(zodMediaSchema).parse(data);
471
491
  }
472
492
  catch (exception) {
@@ -35,6 +35,13 @@ export type MediaSingleOptions = {
35
35
  export type InferMediaSingle<O extends MediaSingleOptions> = O["required"] extends true ? ZodMediaType : ZodMediaType | null | undefined;
36
36
  export declare const mediaSingleSchema: (opts: MediaSingleOptions) => ZodType;
37
37
  export type MediaSingleField = readonly ["media.single", MediaSingleOptions];
38
+ export type MediaMultipleOptions = {
39
+ required?: boolean;
40
+ };
41
+ export type InferMediaMultiple<O extends MediaMultipleOptions> = O["required"] extends true ? ZodMediaType[] : ZodMediaType[] | null | undefined;
42
+ export declare const mediaMultipleSchema: (opts: MediaMultipleOptions) => ZodType;
43
+ export type MediaMultipleField = readonly ["media.multiple", MediaMultipleOptions];
38
44
  export declare const media: {
39
45
  single: <O extends MediaSingleOptions = {}>(options?: O) => ["media.single", O];
46
+ multiple: <O extends MediaMultipleOptions = {}>(options?: O) => ["media.multiple", O];
40
47
  };
@@ -36,10 +36,17 @@ const single = (options = {}) => {
36
36
  };
37
37
  export const mediaSingleSchema = (opts) => {
38
38
  let schema = zodMediaSchema;
39
- // if (opts.nullable) schema = schema.nullable();
40
- // if (opts.optional) schema = schema.optional();
41
39
  if (!opts.required)
42
40
  schema = schema.nullable().optional();
43
41
  return schema;
44
42
  };
45
- export const media = { single };
43
+ const multiple = (options = {}) => {
44
+ return ["media.multiple", options];
45
+ };
46
+ export const mediaMultipleSchema = (opts) => {
47
+ let schema = z.array(zodMediaSchema);
48
+ if (!opts.required)
49
+ schema = schema.nullable().optional();
50
+ return schema;
51
+ };
52
+ export const media = { single, multiple };
@@ -1,7 +1,7 @@
1
1
  import { booleanSchema } from "../fields/boolean";
2
2
  import { dynamicSchema } from "../fields/dynamic";
3
3
  import { enumerationSchema } from "../fields/enumeration";
4
- import { mediaSingleSchema } from "../fields/media";
4
+ import { mediaSingleSchema, mediaMultipleSchema } from "../fields/media";
5
5
  import { numberSchema } from "../fields/number";
6
6
  import { repeatableSchema, singleSchema } from "../fields/component";
7
7
  import { richTextBlocksSchema } from "../fields/richText";
@@ -60,6 +60,11 @@ export const schemaToParser = (schema) => {
60
60
  shape[key] = mediaSingleSchema(args);
61
61
  break;
62
62
  }
63
+ case "media.multiple": {
64
+ const [, args] = field;
65
+ shape[key] = mediaMultipleSchema(args);
66
+ break;
67
+ }
63
68
  case "enumeration": {
64
69
  const [, values, options] = field;
65
70
  shape[key] = enumerationSchema(values, options);
package/package.json CHANGED
@@ -1,16 +1,23 @@
1
1
  {
2
2
  "name": "simple-strapi",
3
- "version": "1.0.0-alpha.26",
3
+ "version": "1.0.0-alpha.28",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
- "readme": "docs/NPM.md",
8
7
  "exports": {
9
8
  ".": {
10
9
  "import": "./dist/index.js",
11
10
  "types": "./dist/index.d.ts"
12
11
  }
13
12
  },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "provenance": true
20
+ },
14
21
  "repository": {
15
22
  "type": "git",
16
23
  "url": "git+https://github.com/hund-studio/simple-strapi.git"
@@ -22,20 +29,15 @@
22
29
  "scripts": {
23
30
  "build": "tsc",
24
31
  "dev": "tsc --watch",
25
- "reset": "tsx scripts/reset.ts main",
26
- "merge": "tsx scripts/merge.ts internal",
27
- "publish:prerelease": "tsx scripts/publish.ts prerelease alpha",
28
- "publish:prepatch": "tsx scripts/publish.ts prepatch alpha",
29
- "publish:patch": "tsx scripts/publish.ts patch",
30
- "publish:preminor": "tsx scripts/publish.ts preminor alpha",
31
- "publish:minor": "tsx scripts/publish.ts minor",
32
- "publish:premajor": "tsx scripts/publish.ts premajor alpha",
33
- "publish:major": "tsx scripts/publish.ts major",
32
+ "copy:readme": "cp docs/readme.md ./README.md",
33
+ "postpublish": "rm ./README.md",
34
+ "prepublishOnly": "npm run build && npm run copy:readme",
35
+ "release:alpha": "npm version prerelease --preid alpha && git push --follow-tags",
36
+ "release:beta": "npm version prerelease --preid beta && git push --follow-tags",
37
+ "release:minor": "npm version minor && git push --follow-tags",
38
+ "release:patch": "npm version patch && git push --follow-tags",
34
39
  "test": "echo \"Error: no test specified\" && exit 1"
35
40
  },
36
- "files": [
37
- "dist"
38
- ],
39
41
  "keywords": [],
40
42
  "author": "",
41
43
  "license": "ISC",