@ryusenkei/maezae 0.12.0 → 0.14.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 +94 -0
- package/dist/delivery.js +108 -6
- package/dist/model.js +10 -0
- package/dist/types/sdk/src/delivery/cache.d.ts +18 -0
- package/dist/types/sdk/src/delivery/fields.d.ts +45 -0
- package/dist/types/sdk/src/delivery/index.d.ts +2 -0
- package/dist/types/sdk/src/delivery/query.d.ts +26 -8
- package/dist/types/sdk/src/delivery.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,100 @@ const client = createDeliveryClient((path, init) =>
|
|
|
31
31
|
);
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## 読み取りの文法(0.14.0 から)
|
|
35
|
+
|
|
36
|
+
型の一覧 `GET /content/v1/<型>` のクエリの契約。仕様は先に一度に決め、CMS は索引の効く範囲から実装する。仕様にあって未実装の書き方は400(`code: "invalid_input"`、`issues` の `message` に未実装の理由)を返す
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
filter[title][contains]=GPU
|
|
40
|
+
filter[published_at][gte]=2026-01-01
|
|
41
|
+
filter[sources.url][begins]=https://example.com # まとまりの中の項目
|
|
42
|
+
filter[or][0][kind][eq]=interview&filter[or][1][topics][in]=sns,culture
|
|
43
|
+
ids=a,b,c
|
|
44
|
+
fields=id,title,writers.name # 参照の先の項目を書くと、その参照を展開
|
|
45
|
+
fields=*,writers.*
|
|
46
|
+
order=-published_at,title
|
|
47
|
+
limit=20&cursor=…
|
|
48
|
+
limit=20&offset=40 # 応答に total
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| 書き方 | 意味 | 実装 |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `filter[key]=v`、`filter[key][eq]=v` | 一致。複数の値の項目は値を含む行 | 済み |
|
|
54
|
+
| `filter[key][ne]=v` | 不一致。値なしの行を含む | 済み(単数の項目) |
|
|
55
|
+
| `filter[key][in]=a,b` | いずれかに一致 | 済み |
|
|
56
|
+
| `filter[key][nin]=a,b` | どれにも一致しない | 未実装 |
|
|
57
|
+
| `filter[key][gte]=v`、`filter[key][lte]=v` | 範囲(両端を含む) | 済み |
|
|
58
|
+
| `filter[key][gt]=v`、`filter[key][lt]=v` | 範囲(両端を含まない) | 未実装 |
|
|
59
|
+
| `filter[key][begins]=v` | 前方一致(大文字と小文字を区別) | 済み(単数の文字) |
|
|
60
|
+
| `filter[key][contains]=v` | 部分一致 | 未実装 |
|
|
61
|
+
| `filter[key][exists]=true` | 値あり(`false` は値なし、空の文字は値なし) | 済み(単数の項目) |
|
|
62
|
+
| `filter[or][i][…]`、`filter[and][i][…]`、`filter[not][i][…]` | 条件の入れ子 | 未実装 |
|
|
63
|
+
| `filter[group.key][…]` | まとまりの中の項目 | 未実装 |
|
|
64
|
+
| `ids=a,b` | 行の id のいずれか(100個まで)。順は `order` の順 | 済み |
|
|
65
|
+
| `fields=…` | 返す項目の選択と参照の展開(下記) | 済み(展開は深さ1) |
|
|
66
|
+
| `order=-a,b` | 並べ替え(3個まで、先頭の `-` で降順、最後に `id` を自動で追加) | 済み |
|
|
67
|
+
| `limit`・`cursor` | ページ送り。応答は `{ items, next }` | 済み |
|
|
68
|
+
| `offset` | 番号のページ送り。応答に `total` | 未実装 |
|
|
69
|
+
|
|
70
|
+
項目の種類ごとに実装済みの演算は次のとおり。非公開の項目は使えない
|
|
71
|
+
|
|
72
|
+
| 項目の種類 | 演算 |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| 文字(単数) | `eq`・`ne`・`in`・`begins`・`exists` |
|
|
75
|
+
| 選択肢・参照(単数) | `eq`・`ne`・`in`・`exists` |
|
|
76
|
+
| 選択肢・参照(複数) | `eq`・`in` |
|
|
77
|
+
| チェックボックス | `eq`(`true`・`false`) |
|
|
78
|
+
| 数値・日付 | `eq`・`ne`・`gte`・`lte`・`exists` |
|
|
79
|
+
| 日時 | `gte`・`lte`・`exists` |
|
|
80
|
+
| `published_at`・`created_at`・`updated_at` | `gte`・`lte` |
|
|
81
|
+
|
|
82
|
+
- 予約の語: `or`・`and`・`not` は最上位の項目の key に使えない(スキーマの保存で拒否する)
|
|
83
|
+
- 選択と展開: `fields` の中の点で参照を展開する。深さの上限は3で、CMS の実装は深さ1。展開した参照は `{ id, …項目 }` で、参照の先が非公開か削除済みなら、単数は `null`、複数は要素を除く
|
|
84
|
+
- `id` は常に返す。`*` はすべての項目と `_meta`、`_meta` は指定したときだけ返す
|
|
85
|
+
- 展開した参照に本文の種類の項目と `_meta` は含めない。一覧で本文の種類の項目を選ぶと400
|
|
86
|
+
- CMS が実装しない範囲: 部分一致・まとまりと複数の値の項目の条件・`total`・`or` の入れ子・深い展開は、DB で重く、クエリの多様さでキャッシュも効かない。重い検索や集計は、導入先が変更の一覧で自分の側に写して行う
|
|
87
|
+
|
|
88
|
+
SDK の `ListQuery` は、スキーマを `as const` の定数で持つときに、項目の key と演算と値の型を絞り込む。`Selected` は `fields` の選択から1件の型を導く(参照先の型はコンテンツスキーマから引く)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createDeliveryClient, listParams, type Selected } from '@ryusenkei/maezae/delivery';
|
|
92
|
+
|
|
93
|
+
const part = await client.page('/parts', { limit: 1, filter: { gtin: '4901234567894' } });
|
|
94
|
+
const fields = ['title', 'writers.name', 'series.*'] as const;
|
|
95
|
+
type Card = Selected<typeof POST, (typeof fields)[number], typeof MODEL>;
|
|
96
|
+
const cards = await client.page<Card>('/post', { limit: 20, fields, filter: { title: { begins: 'GPU' } } });
|
|
97
|
+
const one = await client.find<Card>('/post/p1', listParams({ fields }));
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 回数の制限
|
|
101
|
+
|
|
102
|
+
- 対象は公開のエンドポイント(`https://api.maezae.app/<媒体>/content/v1/…`)だけ。上限は媒体と API の鍵ごとに1分で600回で、データセンターごとの近似で数える
|
|
103
|
+
- 数えるのは、CMS がキャッシュから返せずに DB で計算する要求だけ。キャッシュから返す応答と、`If-None-Match` に対する304は数えない
|
|
104
|
+
- 上限を超えると429(`code: "rate_limited"`)を返す。`Retry-After` の秒数だけ待ってから再試行する
|
|
105
|
+
- 呼び出しを減らす方法: 1ページの取得を `prefetch`(`/bundle`)で1回にまとめ、接続を `cachedDelivery` で包む
|
|
106
|
+
|
|
107
|
+
## 接続のキャッシュ(0.13.0 から)
|
|
108
|
+
|
|
109
|
+
`cachedDelivery` は同じパスとクエリの取得を保持し、配信 API への呼び出しを減らす
|
|
110
|
+
|
|
111
|
+
- 保持から `freshSec` 秒(既定は30)は呼び出さずに返す。過ぎたら前回の ETag で再検証し、304は回数の制限に数えない
|
|
112
|
+
- 429・5xx・接続の失敗のときは、保持した値を `x-stale: 1` つきで返す
|
|
113
|
+
- 下書き(`drafts`)・GET 以外・`prefetch` の `ifNoneMatch` を付けた取得は保持しない
|
|
114
|
+
- 保持の場所: Workers は `caches.default`(データセンターごとに共有)、ほかの実行環境は `memoryDeliveryCache()`
|
|
115
|
+
- `origin` はキーの前半で、媒体ごとに別の値にする
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { cachedDelivery, createDeliveryClient } from '@ryusenkei/maezae/delivery';
|
|
119
|
+
|
|
120
|
+
const origin = 'https://<媒体>.maezae.app';
|
|
121
|
+
const delivery = (path: string, init?: RequestInit) =>
|
|
122
|
+
fetch(`https://api.maezae.app/<媒体>${path}`, { ...init, headers: { ...init?.headers, authorization: `Bearer ${env.CMS_API_KEY}` } });
|
|
123
|
+
const client = createDeliveryClient(cachedDelivery(delivery, { cache: caches.default, origin }));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
公開の操作の反映は、CMS の側の遅れに加えて最大で `freshSec` 秒遅れる
|
|
127
|
+
|
|
34
128
|
## プレビュー
|
|
35
129
|
|
|
36
130
|
- CMS は媒体のサイトのオリジンへ、公開のページと同じパスで署名つきのリクエストを送信
|
package/dist/delivery.js
CHANGED
|
@@ -1,14 +1,115 @@
|
|
|
1
1
|
import { a as optionsOf, l as cropRect, o as typeOf, p as createClock, t as pagePathOf, u as croppedHotspot } from "./page-BnwHL-LC.js";
|
|
2
|
+
// src/delivery/cache.ts
|
|
3
|
+
/** 保持した日時(ミリ秒)を置く内部のヘッダー */
|
|
4
|
+
const CACHED_AT = "x-maezae-cached-at";
|
|
5
|
+
/** 失敗のときに返せるように保持する期間(秒) */
|
|
6
|
+
const KEEP_SEC = 86400;
|
|
7
|
+
const withHeaders = (res, body, set, drop = []) => {
|
|
8
|
+
const headers = new Headers(res.headers);
|
|
9
|
+
for (const k of drop) headers.delete(k);
|
|
10
|
+
for (const [k, v] of Object.entries(set)) headers.set(k, v);
|
|
11
|
+
return new Response(body, {
|
|
12
|
+
status: res.status,
|
|
13
|
+
statusText: res.statusText,
|
|
14
|
+
headers
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 保持つきの配信 API の接続(createDeliveryClient に渡す)
|
|
19
|
+
* - origin: キーの前半(媒体ごとに別の値、例: https://<媒体>.maezae.app)。同じ保持の場所を複数の媒体で使っても混ざらない
|
|
20
|
+
* - freshSec: 呼び出さずに返す秒数(既定は30、CMS のキャッシュの区切りと同じ)
|
|
21
|
+
*/
|
|
22
|
+
function cachedDelivery(delivery, { cache, origin, freshSec = 30 }) {
|
|
23
|
+
const store = async (key, res, body) => {
|
|
24
|
+
try {
|
|
25
|
+
await cache.put(key, withHeaders(res, body, {
|
|
26
|
+
[CACHED_AT]: String(Date.now()),
|
|
27
|
+
"cache-control": `max-age=${KEEP_SEC}`
|
|
28
|
+
}));
|
|
29
|
+
} catch {}
|
|
30
|
+
};
|
|
31
|
+
const fromCache = (res, body, extra = {}) => withHeaders(res, body, extra, [CACHED_AT, "cache-control"]);
|
|
32
|
+
return async (path, init = {}) => {
|
|
33
|
+
const headers = new Headers(init.headers);
|
|
34
|
+
const key = `${origin}${path}`;
|
|
35
|
+
if ((init.method ?? "GET").toUpperCase() !== "GET" || headers.has("if-none-match") || new URL(key).searchParams.get("drafts") === "1") return delivery(path, init);
|
|
36
|
+
const cached = await cache.match(key).catch(() => null) ?? null;
|
|
37
|
+
if (cached && Date.now() - Number(cached.headers.get(CACHED_AT)) < freshSec * 1e3) return fromCache(cached, cached.body);
|
|
38
|
+
const etag = cached?.headers.get("etag");
|
|
39
|
+
if (etag) headers.set("if-none-match", etag);
|
|
40
|
+
let res;
|
|
41
|
+
try {
|
|
42
|
+
res = await delivery(path, {
|
|
43
|
+
...init,
|
|
44
|
+
headers
|
|
45
|
+
});
|
|
46
|
+
} catch (e) {
|
|
47
|
+
if (cached) return fromCache(cached, cached.body, { "x-stale": "1" });
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
if (res.status === 304 && cached) {
|
|
51
|
+
const body = await cached.text();
|
|
52
|
+
await store(key, cached, body);
|
|
53
|
+
return fromCache(cached, body);
|
|
54
|
+
}
|
|
55
|
+
if (res.status === 200 && !res.headers.has("x-partial") && !res.headers.has("x-stale")) {
|
|
56
|
+
const body = await res.text();
|
|
57
|
+
await store(key, res, body);
|
|
58
|
+
return fromCache(res, body);
|
|
59
|
+
}
|
|
60
|
+
if (cached && (res.status === 429 || res.status >= 500)) {
|
|
61
|
+
await res.body?.cancel();
|
|
62
|
+
return fromCache(cached, cached.body, { "x-stale": "1" });
|
|
63
|
+
}
|
|
64
|
+
return res;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** メモリの保持の場所(Workers 以外の実行環境向け、maxEntries を超えたら最も古く使用したものから破棄) */
|
|
68
|
+
function memoryDeliveryCache(maxEntries = 500) {
|
|
69
|
+
const entries = /* @__PURE__ */ new Map();
|
|
70
|
+
return {
|
|
71
|
+
match: async (key) => {
|
|
72
|
+
const v = entries.get(key);
|
|
73
|
+
if (!v) return null;
|
|
74
|
+
entries.delete(key);
|
|
75
|
+
entries.set(key, v);
|
|
76
|
+
return new Response(v.body, {
|
|
77
|
+
status: v.status,
|
|
78
|
+
headers: v.headers
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
put: async (key, res) => {
|
|
82
|
+
entries.delete(key);
|
|
83
|
+
const headers = [];
|
|
84
|
+
res.headers.forEach((v, k) => headers.push([k, v]));
|
|
85
|
+
entries.set(key, {
|
|
86
|
+
status: res.status,
|
|
87
|
+
headers,
|
|
88
|
+
body: await res.text()
|
|
89
|
+
});
|
|
90
|
+
if (entries.size > maxEntries) entries.delete(entries.keys().next().value);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
2
94
|
// src/delivery/query.ts
|
|
3
95
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4
|
-
|
|
96
|
+
const OPS = [
|
|
97
|
+
"eq",
|
|
98
|
+
"ne",
|
|
99
|
+
"in",
|
|
100
|
+
"gte",
|
|
101
|
+
"lte",
|
|
102
|
+
"begins",
|
|
103
|
+
"exists"
|
|
104
|
+
];
|
|
105
|
+
/** in・ids の値の検査(コンマを含む値はクエリで区切れない) */
|
|
5
106
|
const joined = (values) => {
|
|
6
107
|
const parts = values.map(String);
|
|
7
108
|
const bad = parts.find((p) => p.includes(","));
|
|
8
|
-
if (bad !== void 0) throw new Error(`in の値にコンマは不可: ${bad}`);
|
|
109
|
+
if (bad !== void 0) throw new Error(`in・ids の値にコンマは不可: ${bad}`);
|
|
9
110
|
return parts.join(",");
|
|
10
111
|
};
|
|
11
|
-
/** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path
|
|
112
|
+
/** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用、1件の取得の fields にも使用) */
|
|
12
113
|
function listParams(query) {
|
|
13
114
|
const params = {};
|
|
14
115
|
for (const [key, value] of Object.entries(query.filter ?? {})) {
|
|
@@ -17,9 +118,10 @@ function listParams(query) {
|
|
|
17
118
|
params[`filter[${key}]`] = String(value);
|
|
18
119
|
continue;
|
|
19
120
|
}
|
|
20
|
-
if (
|
|
21
|
-
for (const op of ["gte", "lte"]) if (value[op] !== void 0) params[`filter[${key}][${op}]`] = String(value[op]);
|
|
121
|
+
for (const op of OPS) if (value[op] !== void 0) params[`filter[${key}][${op}]`] = op === "in" ? joined(value[op]) : String(value[op]);
|
|
22
122
|
}
|
|
123
|
+
if (query.ids) params.ids = joined(query.ids);
|
|
124
|
+
if (query.fields?.length) params.fields = joined(query.fields);
|
|
23
125
|
if (query.order?.length) params.order = query.order.join(",");
|
|
24
126
|
if (query.limit !== void 0) params.limit = String(query.limit);
|
|
25
127
|
if (query.cursor) params.cursor = query.cursor;
|
|
@@ -306,4 +408,4 @@ function createDeliveryClock(timeZone) {
|
|
|
306
408
|
})
|
|
307
409
|
};
|
|
308
410
|
}
|
|
309
|
-
export { DELIVERY_PATH, DeliveryError, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
|
|
411
|
+
export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
|
package/dist/model.js
CHANGED
|
@@ -393,6 +393,12 @@ const RESERVED_EDITORIAL = [
|
|
|
393
393
|
"review_note",
|
|
394
394
|
"assignee"
|
|
395
395
|
];
|
|
396
|
+
/** 配信 API の絞り込みの論理の演算(filter[or][0][key] など) */
|
|
397
|
+
const RESERVED_QUERY = [
|
|
398
|
+
"or",
|
|
399
|
+
"and",
|
|
400
|
+
"not"
|
|
401
|
+
];
|
|
396
402
|
/** 予約の名前と同じ最上位の key */
|
|
397
403
|
function reservedKeysOf(t) {
|
|
398
404
|
const reserved = [...RESERVED, ...(t.publishing ?? "none") !== "none" ? RESERVED_EDITORIAL : []];
|
|
@@ -451,6 +457,10 @@ const contentType = z.object({
|
|
|
451
457
|
code: "custom",
|
|
452
458
|
message: `型 ${t.id} の key ${k} はシステムの値と同じ名前(別の key に)`
|
|
453
459
|
});
|
|
460
|
+
for (const f of t.fields) if (RESERVED_QUERY.includes(f.key)) ctx.addIssue({
|
|
461
|
+
code: "custom",
|
|
462
|
+
message: `型 ${t.id} の key ${f.key} は配信 API の絞り込みの予約の語(別の key に)`
|
|
463
|
+
});
|
|
454
464
|
for (const k of nestedRules(t.fields)) ctx.addIssue({
|
|
455
465
|
code: "custom",
|
|
456
466
|
message: `型 ${t.id} の項目 ${k}: まとまりの中の項目は決まる値 derive・参照先で固定 fixedBy なし`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Delivery } from './client.js';
|
|
2
|
+
/** Cache API と同じ形の保持の場所(key は URL) */
|
|
3
|
+
export type DeliveryCache = Readonly<{
|
|
4
|
+
match(key: string): Promise<Response | null | undefined>;
|
|
5
|
+
put(key: string, response: Response): Promise<void>;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* 保持つきの配信 API の接続(createDeliveryClient に渡す)
|
|
9
|
+
* - origin: キーの前半(媒体ごとに別の値、例: https://<媒体>.maezae.app)。同じ保持の場所を複数の媒体で使っても混ざらない
|
|
10
|
+
* - freshSec: 呼び出さずに返す秒数(既定は30、CMS のキャッシュの区切りと同じ)
|
|
11
|
+
*/
|
|
12
|
+
export declare function cachedDelivery(delivery: Delivery, { cache, origin, freshSec }: {
|
|
13
|
+
cache: DeliveryCache;
|
|
14
|
+
origin: string;
|
|
15
|
+
freshSec?: number;
|
|
16
|
+
}): Delivery;
|
|
17
|
+
/** メモリの保持の場所(Workers 以外の実行環境向け、maxEntries を超えたら最も古く使用したものから破棄) */
|
|
18
|
+
export declare function memoryDeliveryCache(maxEntries?: number): DeliveryCache;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ContentModel, ContentType, FieldDef } from '../../../model/src/index.js';
|
|
2
|
+
import type { FieldsOf, FieldValue, ItemMeta, ItemOf } from './values.js';
|
|
3
|
+
type PublicField<T extends ContentType> = Exclude<FieldsOf<T>, {
|
|
4
|
+
private: true;
|
|
5
|
+
}>;
|
|
6
|
+
type KeyOf<T extends ContentType> = PublicField<T>['key'];
|
|
7
|
+
type RefField<T extends ContentType> = Extract<PublicField<T>, {
|
|
8
|
+
type: 'reference';
|
|
9
|
+
}>;
|
|
10
|
+
/** fields に書ける1つの指定(項目の key・id・_meta・*、参照の項目は `key.sub` か `key.*`) */
|
|
11
|
+
export type FieldPath<T extends ContentType = ContentType> = 'id' | '_meta' | '*' | KeyOf<T> | `${RefField<T>['key']}.${string}`;
|
|
12
|
+
/** 選択に含む最上位の key(`key.sub` は key) */
|
|
13
|
+
type Head<P> = P extends `${infer K}.${string}` ? K : P;
|
|
14
|
+
/** 参照の項目 K の展開の指定(`K.sub` の sub) */
|
|
15
|
+
type SubOf<P, K extends string> = P extends `${K}.${infer S}` ? S : never;
|
|
16
|
+
/** 参照先の型(M がなければ汎用の型) */
|
|
17
|
+
type TargetOf<F extends FieldDef, M> = M extends ContentModel ? (F extends {
|
|
18
|
+
to: infer To;
|
|
19
|
+
} ? Extract<M['types'][number], {
|
|
20
|
+
id: To;
|
|
21
|
+
}> : never) : never;
|
|
22
|
+
/** 展開した参照先の1件(id と選択した項目、* はすべて) */
|
|
23
|
+
type Expanded<To, S> = [To] extends [never] ? Readonly<{
|
|
24
|
+
id: string;
|
|
25
|
+
} & Record<string, unknown>> : To extends ContentType ? Readonly<{
|
|
26
|
+
id: string;
|
|
27
|
+
} & ('*' extends S ? Omit<ItemOf<To>, '_meta' | 'id'> : Pick<ItemOf<To>, Extract<S, keyof ItemOf<To>>>)> : never;
|
|
28
|
+
/** 選択した1つの項目の値(展開する参照は参照先の1件、単数は null を含み、複数は配列) */
|
|
29
|
+
type SelectedValue<F extends FieldDef, P, M> = [SubOf<P, F['key']>] extends [never] ? FieldValue<F> : F extends {
|
|
30
|
+
multiple: true;
|
|
31
|
+
} ? readonly Expanded<TargetOf<F, M>, SubOf<P, F['key']>>[] : Expanded<TargetOf<F, M>, SubOf<P, F['key']>> | null;
|
|
32
|
+
/**
|
|
33
|
+
* fields の選択から導く1件の型
|
|
34
|
+
* - T: 型の宣言、P: fields の配列の要素(`as const` の配列の `[number]`)、M: 参照先の型を引くコンテンツスキーマ
|
|
35
|
+
*/
|
|
36
|
+
export type Selected<T extends ContentType, P extends string, M = never> = Readonly<{
|
|
37
|
+
id: string;
|
|
38
|
+
} & ('_meta' extends P ? {
|
|
39
|
+
_meta: ItemMeta;
|
|
40
|
+
} : '*' extends P ? {
|
|
41
|
+
_meta: ItemMeta;
|
|
42
|
+
} : unknown) & {
|
|
43
|
+
[F in PublicField<T> as F['key'] extends Head<P> ? F['key'] : '*' extends P ? F['key'] : never]: SelectedValue<F, P, M>;
|
|
44
|
+
}>;
|
|
45
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export { cachedDelivery, memoryDeliveryCache, type DeliveryCache } from './cache.js';
|
|
1
2
|
export { createDeliveryClient, DELIVERY_PATH, DeliveryError, subPath, type BundleResult, type Delivery, type DeliveryClient, type Prefetched } from './client.js';
|
|
2
3
|
export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media.js';
|
|
3
4
|
export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../../model/src/index.js';
|
|
5
|
+
export type { FieldPath, Selected } from './fields.js';
|
|
4
6
|
export { listParams, type ListFilter, type ListQuery, type Page } from './query.js';
|
|
5
7
|
export { createDeliveryClock, fieldOf, limitOf, optionLabels, pagePath, type DeliveryClock } from './schema.js';
|
|
6
8
|
export type { DeliveredAsset, FieldValue, Item, ItemMeta, ItemOf } from './values.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContentType, FieldDef } from '../../../model/src/index.js';
|
|
2
|
+
import type { FieldPath } from './fields.js';
|
|
2
3
|
import type { FieldsOf, Item, SingleValue } from './values.js';
|
|
3
4
|
/** 範囲の指定(両端を含む) */
|
|
4
5
|
type Range<V> = Readonly<{
|
|
@@ -6,20 +7,31 @@ type Range<V> = Readonly<{
|
|
|
6
7
|
lte?: V;
|
|
7
8
|
}>;
|
|
8
9
|
type Scalar<F extends FieldDef> = NonNullable<SingleValue<F>>;
|
|
9
|
-
/**
|
|
10
|
+
/** 演算ごとの値(in は値の配列、exists は値あり・なしの真偽) */
|
|
11
|
+
type Ops<V, O extends FilterOp> = Readonly<{
|
|
12
|
+
[K in O]?: K extends 'in' ? readonly V[] : K extends 'exists' ? boolean : V;
|
|
13
|
+
}>;
|
|
14
|
+
/** 絞り込みの演算(サーバーが実装する範囲) */
|
|
15
|
+
type FilterOp = 'eq' | 'ne' | 'in' | 'gte' | 'lte' | 'begins' | 'exists';
|
|
16
|
+
/** 項目の種類ごとの絞り込みの値(値だけの指定は一致) */
|
|
10
17
|
type FilterValue<F extends FieldDef> = F extends {
|
|
11
18
|
private: true;
|
|
12
19
|
} ? never : F extends {
|
|
13
20
|
type: 'select' | 'reference';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
multiple: true;
|
|
22
|
+
} ? Scalar<F> | Ops<Scalar<F>, 'eq' | 'in'> : F extends {
|
|
23
|
+
multiple: true;
|
|
24
|
+
} ? never : F extends {
|
|
25
|
+
type: 'text';
|
|
26
|
+
} ? string | Ops<string, 'eq' | 'ne' | 'in' | 'begins' | 'exists'> : F extends {
|
|
27
|
+
type: 'select' | 'reference';
|
|
28
|
+
} ? Scalar<F> | Ops<Scalar<F>, 'eq' | 'ne' | 'in' | 'exists'> : F extends {
|
|
17
29
|
type: 'checkbox';
|
|
18
30
|
} ? boolean : F extends {
|
|
19
31
|
type: 'number' | 'date';
|
|
20
|
-
} ? Scalar<F> |
|
|
32
|
+
} ? Scalar<F> | Ops<Scalar<F>, 'eq' | 'ne' | 'gte' | 'lte' | 'exists'> : F extends {
|
|
21
33
|
type: 'datetime';
|
|
22
|
-
} ?
|
|
34
|
+
} ? Ops<string, 'gte' | 'lte' | 'exists'> : never;
|
|
23
35
|
/** 並べ替えできる項目の key(単数の文字・数値・日付・日時・選択肢・チェックボックス) */
|
|
24
36
|
type SortableKey<T extends ContentType> = FieldsOf<T> extends infer F ? (F extends {
|
|
25
37
|
private: true;
|
|
@@ -44,12 +56,16 @@ export type ListFilter<T extends ContentType = ContentType> = Readonly<{
|
|
|
44
56
|
* - limit: 1ページの件数(サーバーの上限は100)
|
|
45
57
|
* - cursor: 前のページの next
|
|
46
58
|
* - order: 並べ替えの key の配列(`-` の接頭辞で降順、最後に id を自動で追加)
|
|
47
|
-
* -
|
|
59
|
+
* - ids: 行の id のいずれか(100個まで、順は order の順)
|
|
60
|
+
* - fields: 返す項目と展開する参照(id は常に返す、展開は深さ1)
|
|
61
|
+
* - filter: 項目の key ごとの絞り込み(複数の値の項目は値を含む行、ne は値なしの行を含む)
|
|
48
62
|
*/
|
|
49
63
|
export type ListQuery<T extends ContentType = ContentType> = Readonly<{
|
|
50
64
|
limit?: number;
|
|
51
65
|
cursor?: string | null;
|
|
52
66
|
order?: readonly (OrderKey<T> | `-${OrderKey<T>}`)[];
|
|
67
|
+
ids?: readonly string[];
|
|
68
|
+
fields?: readonly FieldPath<T>[];
|
|
53
69
|
filter?: ListFilter<T>;
|
|
54
70
|
}>;
|
|
55
71
|
/** ページ送りの応答(next は次のページの cursor、最後のページは null) */
|
|
@@ -62,8 +78,10 @@ type QueryShape = Readonly<{
|
|
|
62
78
|
limit?: number;
|
|
63
79
|
cursor?: string | null;
|
|
64
80
|
order?: readonly string[];
|
|
81
|
+
ids?: readonly string[];
|
|
82
|
+
fields?: readonly string[];
|
|
65
83
|
filter?: Readonly<Record<string, unknown>>;
|
|
66
84
|
}>;
|
|
67
|
-
/** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path
|
|
85
|
+
/** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用、1件の取得の fields にも使用) */
|
|
68
86
|
export declare function listParams(query: QueryShape): Record<string, string>;
|
|
69
87
|
export {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from './delivery/index.js';
|
|
2
|
-
export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from './delivery/index.js';
|
|
1
|
+
export { cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from './delivery/index.js';
|
|
2
|
+
export type { BundleResult, DeliveredAsset, Delivery, DeliveryCache, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, FieldPath, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched, Selected } from './delivery/index.js';
|