@mapled/next 0.5.0 → 0.6.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/README.md CHANGED
@@ -47,6 +47,22 @@ posts[0].expanded?.author; // { id, data: { name: "Ada" } } or null
47
47
 
48
48
  Every call reads the latest **published release** — drafts never leak. Fetches are tagged `mapled` and `mapled:<collection>` and default to `revalidate: 3600`; the webhook below refreshes them the moment someone publishes.
49
49
 
50
+ ### Typed by your schema
51
+
52
+ `npx @mapled/cli types generate` writes `mapled-types.ts`: one interface per collection and single, and a `MapledSchema` that ties them together. Hand it to the client once and every read is typed by its key:
53
+
54
+ ```ts
55
+ import type { MapledSchema } from "./mapled-types";
56
+
57
+ const mapled = createClient<MapledSchema>({ key: process.env.MAPLED_KEY! });
58
+
59
+ const { records } = await mapled.getRecords("articles"); // records[0].data is an Article
60
+ const home = await mapled.getSingle("homepage"); // Homepage | null
61
+ await mapled.getRecords("artcles"); // type error — no such collection
62
+ ```
63
+
64
+ Without a schema the client behaves as before: any key, `Record<string, unknown>` data, or the type you name per call (`getRecords<Article>("articles")`). `createAppClient<MapledSchema>` types live data the same way — `db.create("orders", data)` wants an `Order`. `mapled doctor` tells you when the generated file is behind the schema.
65
+
50
66
  ### Pin a release
51
67
 
52
68
  A page that makes several reads can pin them all to one release, so a publish landing mid-render can't mix two versions:
@@ -131,7 +147,7 @@ The server key reaches only operational collections with access class `public`/`
131
147
 
132
148
  ## API
133
149
 
134
- - `createClient({ key, apiUrl?, release? })` → `getRecords(collection, opts?)`, `getRecord(collection, id, opts?)`, `getRecordBySlug(collection, slug, opts?)`, `getSingle(key, opts?)`, `getRelease()`
150
+ - `createClient({ key, apiUrl?, release? })` → `getRecords(collection, opts?)`, `getRecord(collection, id, opts?)`, `getRecordBySlug(collection, slug, opts?)`, `getSingle(key, opts?)`, `getRelease()`; `createClient<MapledSchema>(…)` with the generated `MapledSchema` types every read by its key (`MapledClient<S>`, `MapledAppClient<S>` name the typed clients)
135
151
  - list `opts`: `filter` (a value for equality, or `{ eq, ne, lt, lte, gt, gte, in, contains }` — text takes eq/ne/in/contains, numbers and dates comparisons, booleans eq/ne, image and relation ids eq/ne/in; a one-to-many relation matches when it includes the id), `sort` (`"field"` / `"-field"`, not on relations or images), `limit` (≤100), `offset`
136
152
  - every read: `expand` (relation keys, dotted for a second level), `fields` (data keys to keep), `release` (pin to a published release), `revalidate`, `tags`; a query the schema can't answer fails with a 400 `INVALID_QUERY` message, and errors carry the API's `code` on `MapledError`
137
153
  - `createRevalidateHandler({ secret, tags? })` — App Router `POST` handler
package/dist/index.d.ts CHANGED
@@ -73,8 +73,63 @@ export declare class MapledError extends Error {
73
73
  code: string | undefined;
74
74
  constructor(status: number, message: string, code?: string);
75
75
  }
76
- export type MapledClient = ReturnType<typeof createClient>;
77
- export type MapledAppClient = ReturnType<typeof createAppClient>;
76
+ /** The shape of the `MapledSchema` that `mapled types generate` writes:
77
+ collections and singles by key, each the type of a record's data.
78
+ Pass it as `createClient<MapledSchema>(…)` and every read is typed by
79
+ its key; without it the client accepts any key and answers
80
+ `Record<string, unknown>` (or the type you name per call). */
81
+ export type ContentSchema = {
82
+ collections?: object;
83
+ singles?: object;
84
+ };
85
+ type Data = Record<string, unknown>;
86
+ type AnyMap = Record<string, Data>;
87
+ type CollectionsOf<S extends ContentSchema> = S["collections"] extends object ? S["collections"] : AnyMap;
88
+ type SinglesOf<S extends ContentSchema> = S["singles"] extends object ? S["singles"] : AnyMap;
89
+ /** Any key on an untyped client; on a typed one, a key it knows — so an
90
+ explicit `getRecords<Article>("articles")` keeps working either way. */
91
+ type LooseKey<S extends ContentSchema, M> = ContentSchema extends S ? string : keyof M & string;
92
+ /** Writes on a typed client take the collection's own type, never a guess. */
93
+ type LooseData<S extends ContentSchema, T> = ContentSchema extends S ? T : never;
94
+ export interface MapledClient<S extends ContentSchema = ContentSchema> {
95
+ /** Published records of a collection, with filters/sort/pagination. */
96
+ getRecords<K extends keyof CollectionsOf<S> & string>(collection: K, opts?: ListOptions): Promise<ListResult<CollectionsOf<S>[K]>>;
97
+ getRecords<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, opts?: ListOptions): Promise<ListResult<T>>;
98
+ /** One published record by id, or null when it isn't in the release. */
99
+ getRecord<K extends keyof CollectionsOf<S> & string>(collection: K, id: string, opts?: ReadOptions): Promise<MapledRecord<CollectionsOf<S>[K]> | null>;
100
+ getRecord<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, id: string, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
101
+ /** One published record by the collection's slug field, or null. */
102
+ getRecordBySlug<K extends keyof CollectionsOf<S> & string>(collection: K, slug: string, opts?: ReadOptions): Promise<MapledRecord<CollectionsOf<S>[K]> | null>;
103
+ getRecordBySlug<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, slug: string, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
104
+ /** The record of a single (e.g. "homepage"), or null before publish. */
105
+ getSingle<K extends keyof SinglesOf<S> & string>(key: K, opts?: ReadOptions): Promise<MapledRecord<SinglesOf<S>[K]> | null>;
106
+ getSingle<T = Data>(key: LooseKey<S, SinglesOf<S>>, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
107
+ /** The release the site is reading right now — the latest published
108
+ one, with the moment it went live — or null before the first
109
+ publish. Never cached: it's the one short-lived pointer of the
110
+ delivery API; pass its number as `release` to pin a render. */
111
+ getRelease(): Promise<{
112
+ release: number;
113
+ publishedAt: string;
114
+ } | null>;
115
+ }
116
+ export interface MapledAppClient<S extends ContentSchema = ContentSchema> {
117
+ list<K extends keyof CollectionsOf<S> & string>(collection: K, opts?: AppListOptions): Promise<{
118
+ records: AppRecord<CollectionsOf<S>[K]>[];
119
+ total: number;
120
+ }>;
121
+ list<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, opts?: AppListOptions): Promise<{
122
+ records: AppRecord<T>[];
123
+ total: number;
124
+ }>;
125
+ get<K extends keyof CollectionsOf<S> & string>(collection: K, id: string): Promise<AppRecord<CollectionsOf<S>[K]> | null>;
126
+ get<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, id: string): Promise<AppRecord<T> | null>;
127
+ create<K extends keyof CollectionsOf<S> & string>(collection: K, data: CollectionsOf<S>[K]): Promise<AppRecord<CollectionsOf<S>[K]>>;
128
+ create<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, data: LooseData<S, T>): Promise<AppRecord<T>>;
129
+ update<K extends keyof CollectionsOf<S> & string>(collection: K, id: string, data: CollectionsOf<S>[K]): Promise<AppRecord<CollectionsOf<S>[K]>>;
130
+ update<T = Data>(collection: LooseKey<S, CollectionsOf<S>>, id: string, data: LooseData<S, T>): Promise<AppRecord<T>>;
131
+ remove(collection: LooseKey<S, CollectionsOf<S>>, id: string): Promise<void>;
132
+ }
78
133
  export type AppRecord<T = Record<string, unknown>> = {
79
134
  id: string;
80
135
  data: T;
@@ -87,41 +142,16 @@ export type AppListOptions = {
87
142
  limit?: number;
88
143
  offset?: number;
89
144
  };
90
- export declare function createClient(config: {
145
+ export declare function createClient<S extends ContentSchema = ContentSchema>(config: {
91
146
  key: string;
92
147
  apiUrl?: string;
93
148
  release?: number;
94
- }): {
95
- /** Published records of a collection, with filters/sort/pagination. */
96
- getRecords<T = Record<string, unknown>>(collection: string, opts?: ListOptions): Promise<ListResult<T>>;
97
- /** One published record by id, or null when it isn't in the release. */
98
- getRecord<T = Record<string, unknown>>(collection: string, id: string, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
99
- /** One published record by the collection's slug field, or null. */
100
- getRecordBySlug<T = Record<string, unknown>>(collection: string, slug: string, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
101
- /** The record of a single (e.g. "homepage"), or null before publish. */
102
- getSingle<T = Record<string, unknown>>(key: string, opts?: ReadOptions): Promise<MapledRecord<T> | null>;
103
- /** The release the site is reading right now — the latest published
104
- one, with the moment it went live — or null before the first
105
- publish. Never cached: it's the one short-lived pointer of the
106
- delivery API; pass its number as `release` to pin a render. */
107
- getRelease(): Promise<{
108
- release: number;
109
- publishedAt: string;
110
- } | null>;
111
- };
149
+ }): MapledClient<S>;
112
150
  /** Live data (operational collections): the site's backend reads and
113
151
  writes records that take effect immediately — no publish involved.
114
152
  Server-side only: the server key must never reach the browser. */
115
- export declare function createAppClient(config: {
153
+ export declare function createAppClient<S extends ContentSchema = ContentSchema>(config: {
116
154
  serverKey: string;
117
155
  apiUrl?: string;
118
- }): {
119
- list<T = Record<string, unknown>>(collection: string, opts?: AppListOptions): Promise<{
120
- records: AppRecord<T>[];
121
- total: number;
122
- }>;
123
- get<T = Record<string, unknown>>(collection: string, id: string): Promise<AppRecord<T> | null>;
124
- create<T = Record<string, unknown>>(collection: string, data: T): Promise<AppRecord<T>>;
125
- update<T = Record<string, unknown>>(collection: string, id: string, data: T): Promise<AppRecord<T>>;
126
- remove(collection: string, id: string): Promise<void>;
127
- };
156
+ }): MapledAppClient<S>;
157
+ export {};
package/dist/index.js CHANGED
@@ -118,8 +118,7 @@ export function createClient(config) {
118
118
  throw err;
119
119
  }
120
120
  };
121
- return {
122
- /** Published records of a collection, with filters/sort/pagination. */
121
+ const client = {
123
122
  async getRecords(collection, opts = {}) {
124
123
  const search = new URLSearchParams();
125
124
  for (const [k, v] of Object.entries(opts.filter ?? {})) {
@@ -144,22 +143,15 @@ export function createClient(config) {
144
143
  const { body } = await call(`/v1/delivery/collections/${encodeURIComponent(collection)}/records`, search, opts, [`mapled:${collection}`]);
145
144
  return body;
146
145
  },
147
- /** One published record by id, or null when it isn't in the release. */
148
146
  getRecord(collection, id, opts = {}) {
149
147
  return one(`/v1/delivery/collections/${encodeURIComponent(collection)}/records/${encodeURIComponent(id)}`, opts, `mapled:${collection}`);
150
148
  },
151
- /** One published record by the collection's slug field, or null. */
152
149
  getRecordBySlug(collection, slug, opts = {}) {
153
150
  return one(`/v1/delivery/collections/${encodeURIComponent(collection)}/records/by-slug/${encodeURIComponent(slug)}`, opts, `mapled:${collection}`);
154
151
  },
155
- /** The record of a single (e.g. "homepage"), or null before publish. */
156
152
  getSingle(key, opts = {}) {
157
153
  return one(`/v1/delivery/singles/${encodeURIComponent(key)}`, opts, `mapled:${key}`);
158
154
  },
159
- /** The release the site is reading right now — the latest published
160
- one, with the moment it went live — or null before the first
161
- publish. Never cached: it's the one short-lived pointer of the
162
- delivery API; pass its number as `release` to pin a render. */
163
155
  async getRelease() {
164
156
  const res = await fetch(`${base}/v1/delivery/release`, {
165
157
  headers: { "x-mapled-key": config.key },
@@ -173,6 +165,8 @@ export function createClient(config) {
173
165
  return { release: body.release, publishedAt: body.publishedAt };
174
166
  },
175
167
  };
168
+ // the same functions behind both overloads: the typed one only narrows
169
+ return client;
176
170
  }
177
171
  /** Live data (operational collections): the site's backend reads and
178
172
  writes records that take effect immediately — no publish involved.
@@ -207,7 +201,7 @@ export function createAppClient(config) {
207
201
  return (await res.json());
208
202
  }
209
203
  const collectionPath = (c) => `/v1/app/collections/${encodeURIComponent(c)}/records`;
210
- return {
204
+ const client = {
211
205
  async list(collection, opts = {}) {
212
206
  const search = new URLSearchParams();
213
207
  for (const [k, v] of Object.entries(opts.filter ?? {})) {
@@ -244,4 +238,5 @@ export function createAppClient(config) {
244
238
  await call("DELETE", `${collectionPath(collection)}/${encodeURIComponent(id)}`);
245
239
  },
246
240
  };
241
+ return client;
247
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapled/next",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Read published Mapled content in a Next.js site \u2014 delivery client, ISR tags, and a revalidation webhook handler.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mapled.io",