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

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,724 @@
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, { path: "products/2024" });
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
+ ### `richText.blocks(options?)`
458
+
459
+ A Strapi rich text blocks field (Strapi v5 block editor format).
460
+
461
+ ```ts
462
+ import { richText } from "simple-strapi";
463
+
464
+ richText.blocks(); // RichTextBlocks | null | undefined
465
+ richText.blocks({ required: true }); // RichTextBlocks
466
+ ```
467
+
468
+ `RichTextBlocks` is an array of block nodes:
469
+
470
+ ```ts
471
+ type RichTextBlocks = Array<
472
+ | { type: "paragraph"; children: ParagraphChild[] }
473
+ | { type: "heading"; level: number; children: ParagraphChild[] }
474
+ | { type: "list"; format: "ordered" | "unordered"; children: ListItemBlock[] }
475
+ >;
476
+
477
+ type ParagraphChild =
478
+ | {
479
+ type: "text";
480
+ text: string;
481
+ bold?: boolean;
482
+ italic?: boolean;
483
+ underline?: boolean;
484
+ strikethrough?: boolean;
485
+ code?: boolean;
486
+ }
487
+ | { type: "link"; url: string; children: ParagraphChild[] };
488
+ ```
489
+
490
+ | Option | Type | Default | Description |
491
+ | ---------- | --------- | ------- | ---------------------------------------------------------------------------------- |
492
+ | `required` | `boolean` | `false` | If true, type is `RichTextBlocks` instead of `RichTextBlocks \| null \| undefined` |
493
+
494
+ TypeScript types: `RichTextBlocksField`, `RichTextBlocksOptions`, `InferRichTextBlocks<O>`, `RichTextBlocks`, `ParagraphChild`, `zodRichTextBlocksSchema`, `paragraphChild`
495
+
496
+ ---
497
+
498
+ ### `component.single(schema, options?)`
499
+
500
+ A Strapi single (non-repeatable) component. Automatically populates nested fields.
501
+
502
+ ```ts
503
+ import { component, text, media } from "simple-strapi";
504
+
505
+ component.single({ title: text(), image: media.single() });
506
+ // { id: number; documentId?: string; title: string | null | undefined; image: MediaType | null | undefined; ... } | null | undefined
507
+
508
+ component.single({ title: text() }, { required: true });
509
+ // { id: number; ...; title: string | null | undefined } — not nullable
510
+ ```
511
+
512
+ | Parameter | Type | Description |
513
+ | --------- | -------- | ----------------------------- |
514
+ | `schema` | `Schema` | Field schema of the component |
515
+
516
+ | Option | Type | Default | Description |
517
+ | ---------- | --------- | ------- | ------------------------------------------- |
518
+ | `required` | `boolean` | `false` | If true, component is not nullable/optional |
519
+
520
+ TypeScript types: `ComponentSingleField`, `ComponentSingleOptions`, `InferComponentSingle<S, O>`
521
+
522
+ ---
523
+
524
+ ### `component.repeatable(schema, options?)`
525
+
526
+ A Strapi repeatable component (array). Automatically populates nested fields.
527
+
528
+ ```ts
529
+ import { component, text } from "simple-strapi";
530
+
531
+ component.repeatable({ label: text(), value: text() });
532
+ // Array<{ id: number; label: string | null | undefined; ... }> | null | undefined
533
+
534
+ component.repeatable({ label: text() }, { required: true });
535
+ // Array<{ id: number; label: string | null | undefined; ... }>
536
+ ```
537
+
538
+ | Parameter | Type | Description |
539
+ | --------- | -------- | ----------------------------------- |
540
+ | `schema` | `Schema` | Field schema of the component items |
541
+
542
+ | Option | Type | Default | Description |
543
+ | ---------- | --------- | ------- | --------------------------------------- |
544
+ | `required` | `boolean` | `false` | If true, array is not nullable/optional |
545
+
546
+ TypeScript types: `ComponentRepeatableField`, `ComponentRepeatableOptions`, `InferComponentRepeatable<S, O>`
547
+
548
+ ---
549
+
550
+ ### `relation.hasMany(schema, options?)`
551
+
552
+ A Strapi relation that returns multiple related entries. Automatically populates the relation.
553
+
554
+ ```ts
555
+ import { relation, text } from "simple-strapi";
556
+
557
+ relation.hasMany({ name: text() });
558
+ // Array<{ id: number; name: string | null | undefined; ... }>
559
+
560
+ relation.hasMany({ name: text() }, { nullable: true, optional: true });
561
+ // Array<...> | null | undefined
562
+ ```
563
+
564
+ | Parameter | Type | Description |
565
+ | --------- | -------- | ----------------------------------- |
566
+ | `schema` | `Schema` | Field schema of the related entries |
567
+
568
+ | Option | Type | Default | Description |
569
+ | ---------- | --------- | ------- | ---------------------------------- |
570
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
571
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
572
+
573
+ TypeScript types: `RelationHasManyField`, `RelationHasManyOptions`, `InferRelationHasMany<S, O>`
574
+
575
+ ---
576
+
577
+ ### `relation.hasOne(schema, options?)`
578
+
579
+ A Strapi relation that returns a single related entry. Automatically populates the relation.
580
+
581
+ ```ts
582
+ import { relation, text } from "simple-strapi";
583
+
584
+ relation.hasOne({ name: text() });
585
+ // { id: number; name: string | null | undefined; ... }
586
+
587
+ relation.hasOne({ name: text() }, { nullable: true });
588
+ // { id: number; ... } | null
589
+ ```
590
+
591
+ | Parameter | Type | Description |
592
+ | --------- | -------- | --------------------------------- |
593
+ | `schema` | `Schema` | Field schema of the related entry |
594
+
595
+ | Option | Type | Default | Description |
596
+ | ---------- | --------- | ------- | ---------------------------------- |
597
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
598
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
599
+
600
+ TypeScript types: `RelationHasOneField`, `RelationHasOneOptions`, `InferRelationHasOne<S, O>`
601
+
602
+ ---
603
+
604
+ ### `dynamic(blocks, options?)`
605
+
606
+ 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`.
607
+
608
+ ```ts
609
+ import { dynamic, text, media, enumeration, component, richText } from "simple-strapi";
610
+
611
+ dynamic({
612
+ "blocks.hero": { title: text({ required: true }), image: media.single() },
613
+ "blocks.content": { body: richText.blocks() },
614
+ });
615
+ // Array<
616
+ // | { __component: "blocks.hero"; title: string; image: MediaType | null | undefined }
617
+ // | { __component: "blocks.content"; body: RichTextBlocks | null | undefined }
618
+ // >
619
+ ```
620
+
621
+ | Parameter | Type | Description |
622
+ | --------- | ------------------------ | -------------------------------------- |
623
+ | `blocks` | `Record<string, Schema>` | Map of component UIDs to their schemas |
624
+
625
+ | Option | Type | Default | Description |
626
+ | ---------- | --------- | ------- | ---------------------------------- |
627
+ | `nullable` | `boolean` | `false` | If true, result can be `null` |
628
+ | `optional` | `boolean` | `false` | If true, result can be `undefined` |
629
+
630
+ TypeScript types: `DynamicField`, `DynamicOptions`, `InferDynamic<B, O>`
631
+
632
+ ---
633
+
634
+ ## TypeScript type utilities
635
+
636
+ All types exported from `simple-strapi`:
637
+
638
+ | Type | Description |
639
+ | ---------------------------- | ----------------------------------------------------------------------------------------- |
640
+ | `Schema` | `Record<string, SchemaField>` — the shape of a schema definition |
641
+ | `SchemaField` | Union of all field tuple types |
642
+ | `InferSchema<S>` | Infers the TypeScript shape from a `Schema` (without default Strapi fields) |
643
+ | `InferSchemaWithDefaults<S>` | Same as `InferSchema<S>` plus `id`, `documentId`, `createdAt`, `updatedAt`, `publishedAt` |
644
+
645
+ ---
646
+
647
+ ## Default Strapi fields
648
+
649
+ Every entity returned by the client automatically includes these fields (regardless of schema):
650
+
651
+ | Field | Type |
652
+ | ------------- | -------------------------------------------- |
653
+ | `id` | `number` |
654
+ | `documentId` | `string \| undefined` |
655
+ | `createdAt` | `string \| undefined` (ISO datetime) |
656
+ | `updatedAt` | `string \| undefined` (ISO datetime) |
657
+ | `publishedAt` | `string \| null \| undefined` (ISO datetime) |
658
+
659
+ ---
660
+
661
+ ## Full example
662
+
663
+ ```ts
664
+ import {
665
+ StrapiClient,
666
+ text,
667
+ number,
668
+ boolean,
669
+ json,
670
+ enumeration,
671
+ media,
672
+ richText,
673
+ component,
674
+ dynamic,
675
+ relation,
676
+ } from "simple-strapi";
677
+
678
+ const client = await StrapiClient.create(process.env.STRAPI_URL!, {
679
+ auth: process.env.STRAPI_TOKEN,
680
+ });
681
+
682
+ const { data: events } = await client.getCollection("events", {
683
+ pagination: false,
684
+ sort: "publishedAt:desc",
685
+ filters: { status: { $eq: "published" } },
686
+ schema: {
687
+ title: text({ required: true }),
688
+ slug: text({ required: true }),
689
+ status: enumeration(["draft", "published", "archived"], { required: true }),
690
+ featured: boolean(),
691
+ price: number(),
692
+ metadata: json(),
693
+ cover: media.single({ required: true }),
694
+ tags: relation.hasMany({ name: text({ required: true }) }),
695
+ location: relation.hasOne({ city: text(), country: text() }),
696
+ header: component.single({
697
+ headline: text({ required: true }),
698
+ background: media.single(),
699
+ }),
700
+ speakers: component.repeatable({
701
+ name: text({ required: true }),
702
+ bio: richText.blocks(),
703
+ avatar: media.single(),
704
+ }),
705
+ blocks: dynamic({
706
+ "blocks.text": { body: richText.blocks({ required: true }) },
707
+ "blocks.gallery": {
708
+ layout: enumeration(["grid", "masonry"]),
709
+ images: component.repeatable({ image: media.single({ required: true }), caption: text() }),
710
+ },
711
+ }),
712
+ },
713
+ });
714
+ ```
715
+
716
+ ---
717
+
718
+ ## License
719
+
720
+ [ISC](../LICENSE)
721
+
722
+ ## Maintainer
723
+
724
+ [@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
@@ -111,6 +111,7 @@ class Client {
111
111
  populate[key] = true;
112
112
  break;
113
113
  case "media.single":
114
+ case "media.multiple":
114
115
  populate[key] = { populate: true };
115
116
  break;
116
117
  case "dynamic":
@@ -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.27",
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",