@ryusenkei/maezae 0.18.1 → 0.20.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
@@ -13,6 +13,7 @@ maezae の SDK(導入先のサイト向け)
13
13
  | `@ryusenkei/maezae/analytics` | サイトの閲覧の記録(CMS の閲覧数の集計と同じ形式) |
14
14
  | `@ryusenkei/maezae/trace` | CMS のログと相関を取るトレースの区間 |
15
15
  | `@ryusenkei/maezae/integrations` | 外の関数・Webhook が受け取る本文の型と署名の検証(ヘッダー `x-cms-signature`) |
16
+ | `@ryusenkei/maezae/schema` | 配信 API の値の型の定義の生成とずれの検査(CLI `maezae-types`、Node) |
16
17
 
17
18
  ## 取得(Cloudflare Workers の service binding)
18
19
 
@@ -131,6 +132,38 @@ try {
131
132
  }
132
133
  ```
133
134
 
135
+ ## スキーマを取る口(0.19.0 から)
136
+
137
+ スキーマの正本は CMS。サイトは `schema.json` の写しを置かず、次の3つに分ける
138
+
139
+ | 値 | 置き場所 | 取り方 |
140
+ | --- | --- | --- |
141
+ | コードが前提にする形(型の id、項目の key と種類、複数か、選択肢の値) | リポに置く生成のファイル | `maezae-types` で生成し、`--check` でずれを止める |
142
+ | 編集で変わる値(表示名、文字数の上限、URL の形) | 置かない | 実行時に `GET /content/v1/types` を読み、`optionLabels`・`limitOf`・`pagePath`(`@ryusenkei/maezae/delivery`)に渡す |
143
+ | 本文の設定と部品の宣言 | 置かない | CMS が保存のときに検査する。知らない部品は描画しない |
144
+
145
+ ```sh
146
+ # 生成(API の鍵は MAEZAE_API_KEY、URL は --url か MAEZAE_DELIVERY_URL)
147
+ npx maezae-types --out src/cms-types.ts --url https://example.com/<媒体>
148
+ # ずれの検査(CI と pre-push。CMS で項目を足すと失敗し、生成し直してコミットする)
149
+ npx maezae-types --out src/cms-types.ts --check
150
+ ```
151
+
152
+ - 生成のファイル: 型ごとに `export type <PascalCase の型の id>`(`ItemOf` と同じ値の形)、全体の対応 `CmsTypes`、型の id の一覧 `TYPE_IDS`。表示名・上限・配信しない項目は含めないので、管理画面での表示名の変更ではずれにならない
153
+ - 整形の対象から外す(`--check` は文字の一致で比べる。改行の CRLF は同じとみなす)
154
+ - 識別子でない key(`lab-note` など)は引用符で囲む(0.20.0 から)
155
+ - 終了の状態(0.20.0 から、`TYPES_EXIT_CODES`): 成功は `0`、ずれは `1`、引数と鍵の不足や CMS の拒否(4xx)は `2`、CMS に届かない(接続の失敗、5xx)ときは `3`。CMS が止まったときに検査を省くなら、`3` だけを通す
156
+ - 同じ処理を Node のスクリプトから呼ぶときは `runTypesCli`(`@ryusenkei/maezae/schema`)。戻り値が終了の状態で、例外を投げない
157
+ - MCP の `get_schema` も同じ文を返す(`format: "typescript"`)。管理画面の「設定 → データ」の型定義のエクスポートも同じ文
158
+
159
+ ## 公開のフォーム(お問い合わせ)
160
+
161
+ お問い合わせのモジュールを有効にした媒体は、toolkit の `@ryusenkei/web/form` の形でフォームを受け付ける
162
+
163
+ - `GET /content/v1/forms/contact`: 項目の宣言 `{ form, label, fields, labels, choices }`。`fields` は web/form の宣言のまま(`parseForm` と画面の検査にそのまま渡せる)、`labels` は項目の名前、`choices` は選択肢の表示。ご用件と上限は CMS の設定から作るので、サイトは写しを持たない
164
+ - `POST /content/v1/forms/contact`: 本文は項目の値の JSON。受け付けは `200 { ok: true, requestId, id }`(`id` は受付の番号、転送だけの設定は `null`)、検査の失敗は `400 { ok: false, error: "invalid", problems }`(文言は web/form の `FORM_MESSAGES_JA` で引く)
165
+ - ボット対策と送信元ごとの回数の制限はサイトの受け取りの処理で行う(web/form の `submissionHandler` の `botCheck`・`rateLimiter`、CMS への転送は `httpDestination` に API の鍵を付ける)
166
+
134
167
  ## 回数の制限
135
168
 
136
169
  - 対象は公開のエンドポイント(`https://api.maezae.app/<媒体>/content/v1/…`)だけ。上限は媒体と API の鍵ごとに1分で600回で、データセンターごとの近似で数える
@@ -243,7 +276,8 @@ if (body.hook === 'publish_check' && body.dryRun) problems.push(...(await verify
243
276
 
244
277
  - `event`: `submitted`(レビューの依頼)・`published`(公開)・`unpublished`(非公開)・`rejected`(差し戻し)。型は `type`(型の ID)で判断する(型の ID を変えても `event` は変わらない)
245
278
  - `id`: イベントの ID(再送しても同じ値、重複の除去に使用)、`item`: 行の ID と題、`by`: 操作した人の表示名、`at`: 出来事の日時
246
- - 拡張機能の出来事(例: お問い合わせの `contact.received`): `{ version: 1, id, event: "contact.received", data, at }`(接頭辞はモジュールの ID で型の ID ではない)
279
+ - 拡張機能の出来事: `{ version: 1, id, event, data, at }`(`event` は拡張機能が宣言した名前で、接頭辞は型の ID ではない)
280
+ - 公開のフォームの送信(お問い合わせの `form.submitted`): toolkit の `@ryusenkei/web/form` の `webhookDestination` と同じ `{ event: "form.submitted", id, form, values, order, receivedAt }`。`id` は送信ごとの UUID、`values` は項目の値(未入力の項目は含めない)、`order` は宣言の項目の順。署名は `x-cms-signature`
247
281
  - 購読: 管理画面の「設定 → 連携」で、送る出来事と対象の型を別々に選ぶ。編集の出来事は「選んだ出来事 × 選んだ型」で届く
248
282
 
249
283
  ```ts
package/dist/analytics.js CHANGED
@@ -6,27 +6,43 @@ const dayIn = (timeZone, at = /* @__PURE__ */ new Date()) => new Intl.DateTimeFo
6
6
  month: "2-digit",
7
7
  day: "2-digit"
8
8
  }).format(at);
9
- // ../analytics/src/record.ts
9
+ // ../analytics/src/view.ts
10
+ /** 位置で持つ記録先の列の順 */
11
+ const VIEW_COLUMNS = [
12
+ "kind",
13
+ "id",
14
+ "path",
15
+ "date",
16
+ "event",
17
+ "project"
18
+ ];
10
19
  const BOT = /bot|crawl|spider|slurp|preview|monitor|headless/i;
11
20
  /** ボットの UA か */
12
21
  const isBot = (userAgent) => BOT.test(userAgent ?? "");
13
- /** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、記録の失敗は無視) */
14
- function recordView(dataset, request, response, { classify, timeZone, project }) {
15
- if (!dataset || request.method !== "GET" || response.status !== 200) return;
16
- if (!response.headers.get("content-type")?.includes("text/html") || isBot(request.headers.get("user-agent"))) return;
22
+ /** 閲覧の出来事(対象: GET で応答が200の HTML・ボット以外、ほかは null) */
23
+ function viewOf(request, response, { classify, timeZone, project }) {
24
+ if (request.method !== "GET" || response.status !== 200) return null;
25
+ if (!response.headers.get("content-type")?.includes("text/html") || isBot(request.headers.get("user-agent"))) return null;
17
26
  const path = new URL(request.url).pathname;
18
27
  const { kind, id } = classify(path);
28
+ return {
29
+ kind,
30
+ id,
31
+ path,
32
+ date: dayIn(timeZone),
33
+ event: "view",
34
+ ...project !== void 0 && { project }
35
+ };
36
+ }
37
+ // src/analytics.ts
38
+ /** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、binding なしと記録の失敗は無視) */
39
+ function recordView(dataset, request, response, options) {
40
+ const view = dataset && viewOf(request, response, options);
41
+ if (!view) return;
19
42
  try {
20
43
  dataset.writeDataPoint({
21
- blobs: [
22
- kind,
23
- id,
24
- path,
25
- dayIn(timeZone),
26
- "view",
27
- project ?? ""
28
- ],
29
- indexes: [project ?? kind]
44
+ blobs: VIEW_COLUMNS.map((key) => view[key] ?? ""),
45
+ indexes: [view.project ?? view.kind]
30
46
  });
31
47
  } catch {}
32
48
  }
@@ -0,0 +1,137 @@
1
+ // src/delivery/query.ts
2
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3
+ const OPS = [
4
+ "eq",
5
+ "ne",
6
+ "in",
7
+ "gte",
8
+ "lte",
9
+ "begins",
10
+ "exists"
11
+ ];
12
+ /** in・ids の値の検査(コンマを含む値はクエリで区切れない) */
13
+ const joined = (values) => {
14
+ const parts = values.map(String);
15
+ const bad = parts.find((p) => p.includes(","));
16
+ if (bad !== void 0) throw new Error(`in・ids の値にコンマは不可: ${bad}`);
17
+ return parts.join(",");
18
+ };
19
+ /** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用、1件の取得の fields にも使用) */
20
+ function listParams(query) {
21
+ const params = {};
22
+ for (const [key, value] of Object.entries(query.filter ?? {})) {
23
+ if (value === void 0) continue;
24
+ if (!isRecord(value)) {
25
+ params[`filter[${key}]`] = String(value);
26
+ continue;
27
+ }
28
+ for (const op of OPS) if (value[op] !== void 0) params[`filter[${key}][${op}]`] = op === "in" ? joined(value[op]) : String(value[op]);
29
+ }
30
+ if (query.ids) params.ids = joined(query.ids);
31
+ if (query.fields?.length) params.fields = joined(query.fields);
32
+ if (query.order?.length) params.order = query.order.join(",");
33
+ if (query.limit !== void 0) params.limit = String(query.limit);
34
+ if (query.cursor) params.cursor = query.cursor;
35
+ return params;
36
+ }
37
+ // src/delivery/client.ts
38
+ /** 配信 API のパスの前半 */
39
+ const DELIVERY_PATH = "/content/v1";
40
+ /** 配信 API の取得の失敗(call: 失敗した経路、status: 応答のステータス) */
41
+ var DeliveryError = class extends Error {
42
+ call;
43
+ status;
44
+ constructor(call, status, body) {
45
+ super(`配信 API ${call}: ${status}${body ? ` ${body}` : ""}`);
46
+ this.name = "DeliveryError";
47
+ this.call = call;
48
+ this.status = status;
49
+ }
50
+ };
51
+ /** 配信 API の中のパスとクエリ(一式の path と結果のキー) */
52
+ const subPath = (path, params = {}) => {
53
+ const q = new URLSearchParams(params).toString();
54
+ return `${path}${q ? `?${q}` : ""}`;
55
+ };
56
+ /**
57
+ * 配信 API の読み取り
58
+ * - drafts: 下書き・予約なども含める(プレビュー用、署名は接続の側で付与)
59
+ * - at: 公開の基準の日時(なしは今)
60
+ */
61
+ function createDeliveryClient(delivery, { drafts = false, at } = {}) {
62
+ const query = (params = {}) => new URLSearchParams({
63
+ ...params,
64
+ ...drafts ? { drafts: "1" } : {},
65
+ ...at ? { at } : {}
66
+ });
67
+ const url = (path, params) => {
68
+ const q = query(params).toString();
69
+ return `${DELIVERY_PATH}${path}${q ? `?${q}` : ""}`;
70
+ };
71
+ let bundled = /* @__PURE__ */ new Map();
72
+ /** 取得(404は undefined、ほかの失敗は例外、一式で取得済みならその結果) */
73
+ const find = async (path, params = {}) => {
74
+ const hit = bundled.get(subPath(path, params));
75
+ if (hit) {
76
+ if (hit.status === 404) return void 0;
77
+ if (hit.status < 200 || hit.status >= 300) throw new DeliveryError(path, hit.status, JSON.stringify(hit.body));
78
+ return hit.body;
79
+ }
80
+ const res = await delivery(url(path, params));
81
+ if (res.status === 404) return void 0;
82
+ if (!res.ok) throw new DeliveryError(path, res.status, await res.text());
83
+ return await res.json();
84
+ };
85
+ /** 取得(404も例外) */
86
+ const get = async (path, params) => {
87
+ const v = await find(path, params);
88
+ if (v === void 0) throw new DeliveryError(path, 404, "");
89
+ return v;
90
+ };
91
+ /**
92
+ * 型の一覧の1ページ(path は型の一覧の経路、例: /post)
93
+ * - query: 絞り込み・並べ替えと limit、次のページは前の応答の next を cursor に
94
+ * - 一式で同じ path とクエリを取得済みならその結果(一式の path は `subPath(path, listParams(query))`)
95
+ */
96
+ const page = (path, query) => get(path, listParams(query));
97
+ /**
98
+ * 経路の一覧を一式の1回で取得(paths は配信 API の中のパスとクエリ、`*` の区画は一覧の id ごとに展開)
99
+ * - ifNoneMatch: 前回の ETag(一致すれば notModified で、結果は未取得)
100
+ */
101
+ const prefetch = async (paths, { ifNoneMatch } = {}) => {
102
+ const q = query();
103
+ for (const p of paths) q.append("path", p);
104
+ const res = await delivery(`${DELIVERY_PATH}/bundle?${q}`, ifNoneMatch ? { headers: { "if-none-match": ifNoneMatch } } : {});
105
+ if (res.status === 304) return {
106
+ notModified: true,
107
+ etag: ifNoneMatch ?? null,
108
+ stale: false,
109
+ partial: false
110
+ };
111
+ if (!res.ok) throw new DeliveryError("/bundle", res.status, await res.text());
112
+ const { results } = await res.json();
113
+ bundled = new Map(Object.entries(results));
114
+ return {
115
+ notModified: false,
116
+ etag: res.headers.get("etag"),
117
+ stale: res.headers.has("x-stale"),
118
+ partial: res.headers.has("x-partial")
119
+ };
120
+ };
121
+ /**
122
+ * 変更の一覧の1ページ(since は前回の next、null は全件の取得から)
123
+ * - 410(保存の期間を過ぎた cursor)は DeliveryError、since なしで取り直す
124
+ */
125
+ const changes = (since, { limit } = {}) => get("/changes", {
126
+ ...since ? { since } : {},
127
+ ...limit !== void 0 ? { limit: String(limit) } : {}
128
+ });
129
+ return {
130
+ find,
131
+ get,
132
+ page,
133
+ prefetch,
134
+ changes
135
+ };
136
+ }
137
+ export { listParams as a, subPath as i, DeliveryError as n, createDeliveryClient as r, DELIVERY_PATH as t };
package/dist/delivery.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { a as listParams, i as subPath, n as DeliveryError, r as createDeliveryClient, t as DELIVERY_PATH } from "./client-ClY3gSSd.js";
1
2
  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
3
  // src/delivery/cache.ts
3
4
  /** 保持した日時(ミリ秒)を置く内部のヘッダー */
@@ -106,142 +107,6 @@ async function readChanges(client, since, apply, { limit } = {}) {
106
107
  if (!page.more) return cursor;
107
108
  }
108
109
  }
109
- // src/delivery/query.ts
110
- const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
111
- const OPS = [
112
- "eq",
113
- "ne",
114
- "in",
115
- "gte",
116
- "lte",
117
- "begins",
118
- "exists"
119
- ];
120
- /** in・ids の値の検査(コンマを含む値はクエリで区切れない) */
121
- const joined = (values) => {
122
- const parts = values.map(String);
123
- const bad = parts.find((p) => p.includes(","));
124
- if (bad !== void 0) throw new Error(`in・ids の値にコンマは不可: ${bad}`);
125
- return parts.join(",");
126
- };
127
- /** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用、1件の取得の fields にも使用) */
128
- function listParams(query) {
129
- const params = {};
130
- for (const [key, value] of Object.entries(query.filter ?? {})) {
131
- if (value === void 0) continue;
132
- if (!isRecord(value)) {
133
- params[`filter[${key}]`] = String(value);
134
- continue;
135
- }
136
- for (const op of OPS) if (value[op] !== void 0) params[`filter[${key}][${op}]`] = op === "in" ? joined(value[op]) : String(value[op]);
137
- }
138
- if (query.ids) params.ids = joined(query.ids);
139
- if (query.fields?.length) params.fields = joined(query.fields);
140
- if (query.order?.length) params.order = query.order.join(",");
141
- if (query.limit !== void 0) params.limit = String(query.limit);
142
- if (query.cursor) params.cursor = query.cursor;
143
- return params;
144
- }
145
- // src/delivery/client.ts
146
- /** 配信 API のパスの前半 */
147
- const DELIVERY_PATH = "/content/v1";
148
- /** 配信 API の取得の失敗(call: 失敗した経路、status: 応答のステータス) */
149
- var DeliveryError = class extends Error {
150
- call;
151
- status;
152
- constructor(call, status, body) {
153
- super(`配信 API ${call}: ${status}${body ? ` ${body}` : ""}`);
154
- this.name = "DeliveryError";
155
- this.call = call;
156
- this.status = status;
157
- }
158
- };
159
- /** 配信 API の中のパスとクエリ(一式の path と結果のキー) */
160
- const subPath = (path, params = {}) => {
161
- const q = new URLSearchParams(params).toString();
162
- return `${path}${q ? `?${q}` : ""}`;
163
- };
164
- /**
165
- * 配信 API の読み取り
166
- * - drafts: 下書き・予約なども含める(プレビュー用、署名は接続の側で付与)
167
- * - at: 公開の基準の日時(なしは今)
168
- */
169
- function createDeliveryClient(delivery, { drafts = false, at } = {}) {
170
- const query = (params = {}) => new URLSearchParams({
171
- ...params,
172
- ...drafts ? { drafts: "1" } : {},
173
- ...at ? { at } : {}
174
- });
175
- const url = (path, params) => {
176
- const q = query(params).toString();
177
- return `${DELIVERY_PATH}${path}${q ? `?${q}` : ""}`;
178
- };
179
- let bundled = /* @__PURE__ */ new Map();
180
- /** 取得(404は undefined、ほかの失敗は例外、一式で取得済みならその結果) */
181
- const find = async (path, params = {}) => {
182
- const hit = bundled.get(subPath(path, params));
183
- if (hit) {
184
- if (hit.status === 404) return void 0;
185
- if (hit.status < 200 || hit.status >= 300) throw new DeliveryError(path, hit.status, JSON.stringify(hit.body));
186
- return hit.body;
187
- }
188
- const res = await delivery(url(path, params));
189
- if (res.status === 404) return void 0;
190
- if (!res.ok) throw new DeliveryError(path, res.status, await res.text());
191
- return await res.json();
192
- };
193
- /** 取得(404も例外) */
194
- const get = async (path, params) => {
195
- const v = await find(path, params);
196
- if (v === void 0) throw new DeliveryError(path, 404, "");
197
- return v;
198
- };
199
- /**
200
- * 型の一覧の1ページ(path は型の一覧の経路、例: /post)
201
- * - query: 絞り込み・並べ替えと limit、次のページは前の応答の next を cursor に
202
- * - 一式で同じ path とクエリを取得済みならその結果(一式の path は `subPath(path, listParams(query))`)
203
- */
204
- const page = (path, query) => get(path, listParams(query));
205
- /**
206
- * 経路の一覧を一式の1回で取得(paths は配信 API の中のパスとクエリ、`*` の区画は一覧の id ごとに展開)
207
- * - ifNoneMatch: 前回の ETag(一致すれば notModified で、結果は未取得)
208
- */
209
- const prefetch = async (paths, { ifNoneMatch } = {}) => {
210
- const q = query();
211
- for (const p of paths) q.append("path", p);
212
- const res = await delivery(`${DELIVERY_PATH}/bundle?${q}`, ifNoneMatch ? { headers: { "if-none-match": ifNoneMatch } } : {});
213
- if (res.status === 304) return {
214
- notModified: true,
215
- etag: ifNoneMatch ?? null,
216
- stale: false,
217
- partial: false
218
- };
219
- if (!res.ok) throw new DeliveryError("/bundle", res.status, await res.text());
220
- const { results } = await res.json();
221
- bundled = new Map(Object.entries(results));
222
- return {
223
- notModified: false,
224
- etag: res.headers.get("etag"),
225
- stale: res.headers.has("x-stale"),
226
- partial: res.headers.has("x-partial")
227
- };
228
- };
229
- /**
230
- * 変更の一覧の1ページ(since は前回の next、null は全件の取得から)
231
- * - 410(保存の期間を過ぎた cursor)は DeliveryError、since なしで取り直す
232
- */
233
- const changes = (since, { limit } = {}) => get("/changes", {
234
- ...since ? { since } : {},
235
- ...limit !== void 0 ? { limit: String(limit) } : {}
236
- });
237
- return {
238
- find,
239
- get,
240
- page,
241
- prefetch,
242
- changes
243
- };
244
- }
245
110
  // ../model/src/mediaUrl.ts
246
111
  /** 配信する形式と拡張子 */
247
112
  const EXTENSIONS = {
package/dist/dev.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as SIGNED_HEADERS, s as signedHeaders } from "./src-C6YqllXw.js";
1
+ import { i as SIGNED_HEADERS, s as signedHeaders } from "./src-BMOqN7Xs.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { createServer } from "node:http";
@@ -1,4 +1,4 @@
1
- import { a as openValue, i as SIGNED_HEADERS, l as verifyRequest } from "./src-C6YqllXw.js";
1
+ import { a as openValue, i as SIGNED_HEADERS, l as verifyRequest } from "./src-BMOqN7Xs.js";
2
2
  import "./src-BrLwIWaT.js";
3
3
  // ../cms-contract/src/embeds.ts
4
4
  /** URL のクエリの名前(署名つきの文脈) */
package/dist/model.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { c as coverStart, d as objectPosition, f as stillOf, i as acceptsRole, l as cropRect, m as isDateOnly, n as publicPathOf, o as typeOf, p as createClock, r as FIELD_ROLES, s as coverPosition, t as pagePathOf, u as croppedHotspot } from "./page-BnwHL-LC.js";
2
2
  import { i as CORE_COMPONENTS, t as declaredComponentsSchema } from "./declared-Qo-mWnDd.js";
3
+ import { t as typesSourceOf } from "./typesSource-DKTe9a2-.js";
3
4
  import "./src-BrLwIWaT.js";
4
5
  import { z } from "zod";
5
6
  // ../model/src/asset.ts
@@ -751,4 +752,4 @@ const DEFAULT_SCREENS = {
751
752
  "updated_at"
752
753
  ] }
753
754
  };
754
- export { CONFIG_FORMAT, DEFAULT_SCHEMA, DEFAULT_SCREENS, IMAGE_TYPE, VIDEO_TYPE, coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, parseModel, parseScreens, publicPathOf, stillOf, typeOf };
755
+ export { CONFIG_FORMAT, DEFAULT_SCHEMA, DEFAULT_SCREENS, IMAGE_TYPE, VIDEO_TYPE, coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, parseModel, parseScreens, publicPathOf, stillOf, typeOf, typesSourceOf };
package/dist/preview.js CHANGED
@@ -1 +1 @@
1
- export { i as SIGNED_HEADERS, o as signParts, s as signedHeaders, c as verifyParts, l as verifyRequest } from "./src-C6YqllXw.js";
1
+ export { i as SIGNED_HEADERS, o as signParts, s as signedHeaders, c as verifyParts, l as verifyRequest } from "./src-BMOqN7Xs.js";
@@ -0,0 +1,114 @@
1
+ import { t as typesSourceOf } from "./typesSource-DKTe9a2-.js";
2
+ import { t as DELIVERY_PATH } from "./client-ClY3gSSd.js";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ // src/schema.ts
5
+ /**
6
+ * maezae-types の終了の状態
7
+ * - drift: --check でファイルが CMS のスキーマと違う
8
+ * - failed: 引数と鍵の不足、CMS の拒否(4xx)、応答の形の誤り、ファイルの読み書きの失敗
9
+ * - unreachable: CMS に届かない(接続の失敗、5xx)
10
+ */
11
+ const TYPES_EXIT_CODES = {
12
+ ok: 0,
13
+ drift: 1,
14
+ failed: 2,
15
+ unreachable: 3
16
+ };
17
+ /** 標準出力と標準エラーの文 */
18
+ const MESSAGES = {
19
+ usage: "usage: maezae-types --out <file> [--check] [--url <delivery API URL>](API key: MAEZAE_API_KEY)",
20
+ missingUrl: "missing_url: --url か MAEZAE_DELIVERY_URL に配信 API の URL(例: https://example.com/<媒体>)",
21
+ missingKey: "missing_api_key: 環境変数 MAEZAE_API_KEY",
22
+ fetchFailed: (status, body) => `fetch_failed: ${status} ${body.slice(0, 200)}`,
23
+ invalidResponse: "invalid_response: 配信 API の /types の応答に types がない",
24
+ unreachable: (url, reason) => `cms_unreachable: ${url} ${reason}`,
25
+ drift: (file) => `schema_drift: ${file} が CMS のスキーマと違う(maezae-types --out ${file} で生成し直す)`,
26
+ upToDate: (file) => `up_to_date: ${file}`,
27
+ written: (file) => `written: ${file}`
28
+ };
29
+ /** 引数(--name value と --flag) */
30
+ function argsOf(argv) {
31
+ const value = (name) => {
32
+ const i = argv.indexOf(name);
33
+ return i >= 0 ? argv[i + 1] : void 0;
34
+ };
35
+ return {
36
+ out: value("--out"),
37
+ url: value("--url"),
38
+ check: argv.includes("--check")
39
+ };
40
+ }
41
+ /** 配信 API の /types の応答の失敗(status は HTTP の状態) */
42
+ var TypesResponseError = class extends Error {
43
+ status;
44
+ constructor(message, status) {
45
+ super(message);
46
+ this.status = status;
47
+ }
48
+ };
49
+ /**
50
+ * 配信 API の型の一覧(/types、表示名・選択肢・上限・ページの URL の形を含む。実行時に読む値もここから)
51
+ * - 例外: 2xx でなければ `fetch_failed: <状態> <本文>`、本文に types がなければ `invalid_response`、接続の失敗は fetcher の例外のまま
52
+ */
53
+ async function fetchDeliveredTypes(url, apiKey, fetcher = fetch) {
54
+ const res = await fetcher(`${url.replace(/\/+$/, "")}${DELIVERY_PATH}/types`, { headers: { authorization: `Bearer ${apiKey}` } });
55
+ if (!res.ok) throw new TypesResponseError(MESSAGES.fetchFailed(res.status, await res.text().catch(() => "")), res.status);
56
+ const types = (await res.json().catch(() => null))?.types;
57
+ if (!Array.isArray(types)) throw new TypesResponseError(MESSAGES.invalidResponse, res.status);
58
+ return types;
59
+ }
60
+ const messageOf = (e) => {
61
+ const cause = e instanceof Error && e.cause instanceof Error ? `(${e.cause.message})` : "";
62
+ return `${e instanceof Error ? e.message : String(e)}${cause}`;
63
+ };
64
+ /** 取得の失敗の終了の状態と文(接続の失敗と 5xx は unreachable) */
65
+ function fetchFailureOf(url, e) {
66
+ if (!(e instanceof TypesResponseError)) return {
67
+ code: TYPES_EXIT_CODES.unreachable,
68
+ message: MESSAGES.unreachable(url, messageOf(e))
69
+ };
70
+ return {
71
+ code: e.status >= 500 ? TYPES_EXIT_CODES.unreachable : TYPES_EXIT_CODES.failed,
72
+ message: e.message
73
+ };
74
+ }
75
+ /** 生成の結果をファイルに書くか、--check でファイルと比べる(戻り値は終了の状態) */
76
+ function writeOrCheck(out, source, check) {
77
+ if (!check) {
78
+ writeFileSync(out, source);
79
+ console.log(MESSAGES.written(out));
80
+ return TYPES_EXIT_CODES.ok;
81
+ }
82
+ let current = null;
83
+ try {
84
+ current = readFileSync(out, "utf8").replace(/\r\n/g, "\n");
85
+ } catch {
86
+ current = null;
87
+ }
88
+ if (current !== source) return console.error(MESSAGES.drift(out)), TYPES_EXIT_CODES.drift;
89
+ console.log(MESSAGES.upToDate(out));
90
+ return TYPES_EXIT_CODES.ok;
91
+ }
92
+ /** CLI の本体(戻り値は終了の状態 TYPES_EXIT_CODES、失敗は例外にせず標準エラーと終了の状態で返す) */
93
+ async function runTypesCli(argv, env, fetcher = fetch) {
94
+ const { out, url = env.MAEZAE_DELIVERY_URL, check } = argsOf(argv);
95
+ if (!out) return console.error(MESSAGES.usage), TYPES_EXIT_CODES.failed;
96
+ if (!url) return console.error(MESSAGES.missingUrl), TYPES_EXIT_CODES.failed;
97
+ const apiKey = env.MAEZAE_API_KEY;
98
+ if (!apiKey) return console.error(MESSAGES.missingKey), TYPES_EXIT_CODES.failed;
99
+ let types;
100
+ try {
101
+ types = await fetchDeliveredTypes(url, apiKey, fetcher);
102
+ } catch (e) {
103
+ const failure = fetchFailureOf(url, e);
104
+ console.error(failure.message);
105
+ return failure.code;
106
+ }
107
+ try {
108
+ return writeOrCheck(out, typesSourceOf({ types }), check);
109
+ } catch (e) {
110
+ console.error(messageOf(e));
111
+ return TYPES_EXIT_CODES.failed;
112
+ }
113
+ }
114
+ export { fetchDeliveredTypes as n, runTypesCli as r, TYPES_EXIT_CODES as t };
@@ -0,0 +1 @@
1
+ export * from './types/sdk/src/schema.js';
package/dist/schema.js ADDED
@@ -0,0 +1 @@
1
+ export { t as TYPES_EXIT_CODES, n as fetchDeliveredTypes, r as runTypesCli } from "./schema-CUrLZWsb.js";
@@ -1,32 +1,9 @@
1
- // ../access/src/jwt.ts
2
- const CERTS_TTL_MS = 3e5;
3
- const REFRESH_INTERVAL_MS = 3e4;
4
- /** 公開鍵の一覧を保持する取得(取得の失敗は保持しない) */
5
- function cachedCerts(fetcher, clock = Date.now) {
6
- const cache = /* @__PURE__ */ new Map();
7
- return async (url, refresh = false) => {
8
- const hit = cache.get(url);
9
- const age = hit ? clock() - hit.at : Infinity;
10
- if (hit && age < (refresh ? REFRESH_INTERVAL_MS : CERTS_TTL_MS)) return hit;
11
- const fresh = await fetcher(url);
12
- cache.set(url, {
13
- keys: fresh.keys,
14
- at: clock()
15
- });
16
- return fresh;
17
- };
18
- }
19
- cachedCerts(async (url) => {
20
- const res = await fetch(url);
21
- if (!res.ok) throw new Error(`JWKS の取得に失敗(${res.status})`);
22
- return res.json();
23
- });
24
- /** base64url の復号 */
25
- const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
26
1
  // ../access/src/signed.ts
27
2
  const enc = new TextEncoder();
28
3
  /** 既定で許す時刻のずれ(秒) */
29
4
  const MAX_SKEW_SEC = 300;
5
+ /** base64url の復号 */
6
+ const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
30
7
  /** 経路ごとのヘッダーの名前(署名と主体) */
31
8
  const SIGNED_HEADERS = {
32
9
  /** MCP → CMS の署名(鍵は MCP_TOKEN) */
@@ -195,7 +172,7 @@ function startEdgeSpan({ name, request, sampleRate = 1, write = (line) => consol
195
172
  };
196
173
  write(JSON.stringify({
197
174
  level: "info",
198
- msg: "span",
175
+ event: "span",
199
176
  ...span
200
177
  }));
201
178
  }
package/dist/trace.js CHANGED
@@ -1 +1 @@
1
- export { r as TRACEPARENT_HEADER, t as sampleRateOf, n as withEdgeSpan } from "./src-C6YqllXw.js";
1
+ export { r as TRACEPARENT_HEADER, t as sampleRateOf, n as withEdgeSpan } from "./src-BMOqN7Xs.js";
@@ -1,12 +1,9 @@
1
- type Jwk = JsonWebKey & {
2
- kid?: string;
3
- };
4
- /** 公開鍵の一覧の取得(引数は JWKS の URL、refresh は保持した値を使用せずに再取得、テストで差し替え) */
5
- export type FetchCerts = (jwksUrl: string, refresh?: boolean) => Promise<{
6
- keys: Jwk[];
7
- }>;
8
1
  /** JWT のクレーム(検証を通った値) */
9
2
  export type JwtClaims = Readonly<Record<string, unknown>>;
3
+ /** 公開鍵の一覧の取得の差し替え(テスト用、引数は JWKS の URL) */
4
+ export type FetchCerts = (jwksUrl: string) => Promise<{
5
+ keys: JsonWebKey[];
6
+ }>;
10
7
  /** JWT の検証の設定 */
11
8
  export type JwtOptions = Readonly<{
12
9
  /** 発行元(iss と完全に一致) */
@@ -15,15 +12,12 @@ export type JwtOptions = Readonly<{
15
12
  audience: string | readonly string[];
16
13
  /** 公開鍵の一覧の URL */
17
14
  jwksUrl: string;
15
+ /** 現在時刻(ミリ秒、テスト用) */
18
16
  now?: number;
19
17
  certs?: FetchCerts;
20
18
  }>;
21
- /** 公開鍵の一覧を保持する取得(取得の失敗は保持しない) */
22
- export declare function cachedCerts(fetcher: FetchCerts, clock?: () => number): FetchCerts;
23
- /** base64url の復号 */
24
- export declare const decodeBase64Url: (s: string) => Uint8Array<ArrayBuffer>;
25
19
  /** 検証を通った JWT のクレーム(署名・発行元・宛先・期限のどれかが違えば null) */
26
- export declare function verifyJwt(token: string, { issuer, audience, jwksUrl, now, certs }: JwtOptions): Promise<JwtClaims | null>;
20
+ export declare function verifyJwt(token: string, options: JwtOptions): Promise<JwtClaims | null>;
27
21
  /**
28
22
  * Cloudflare Access の JWT を検証する設定(teamDomain は <チーム>.cloudflareaccess.com、aud はアプリの AUD)
29
23
  * - aud: カンマ区切りで複数のアプリ(ホスト名ごとにアプリが分かれる管理画面、どのアプリを通すかは Access がホスト名で判定)
@@ -39,4 +33,3 @@ export declare function verifyAccessJwt(token: string, { teamDomain, aud, now, c
39
33
  now?: number;
40
34
  certs?: FetchCerts;
41
35
  }): Promise<string | null>;
42
- export {};
@@ -1,5 +1,5 @@
1
- export { recordView, type Classify, type Dataset } from './record.js';
1
+ export { type Classify, VIEW_COLUMNS, type ViewEvent, viewOf, type ViewOptions } from './view.js';
2
2
  export { syncCron } from './schedule.js';
3
3
  export { isViewsCron, sharedViews, viewsFor, viewsSince, viewsSyncOf } from './shared.js';
4
- export { analyticsEngine, type DailyRow, fixedSource, type Source, projectDaily, type ProjectSource, renamedSource } from './source.js';
4
+ export { type DailyRow, fixedSource, type ProjectSource, renamedSource, type Source } from './source.js';
5
5
  export { readDaily, syncDaily } from './store.js';
@@ -9,27 +9,11 @@ export type DailyRow = {
9
9
  export type Source = {
10
10
  daily(since: string): Promise<DailyRow[]>;
11
11
  };
12
- /** Analytics Engine の接続の設定(token は Account Analytics の読み取りの API トークン、project は絞り込む媒体の識別子) */
13
- type AnalyticsEngineOptions = {
14
- accountId: string;
15
- token: string;
16
- dataset: string;
17
- project?: string;
18
- };
19
- /** 1回の問い合わせの行数 */
20
- export declare const PAGE_ROWS = 10000;
21
- /** 問い合わせの回数の上限(PAGE_ROWS との積が1回で集計できる行数) */
22
- export declare const MAX_PAGES = 20;
23
- /** Analytics Engine の SQL API の読み込み元 */
24
- export declare function analyticsEngine({ accountId, token, dataset, project }: AnalyticsEngineOptions): Source;
25
12
  /** 全媒体の読み込み元の型(since 以降の日付の閲覧数を媒体の識別子ごとに) */
26
13
  export type ProjectSource = {
27
14
  dailyByProject(since: string): Promise<ReadonlyMap<string, DailyRow[]>>;
28
15
  };
29
- /** 全媒体を1回の問い合わせで集計する Analytics Engine の読み込み元(媒体の列が空の記録は除外) */
30
- export declare function projectDaily(options: Omit<AnalyticsEngineOptions, 'project'>): ProjectSource;
31
16
  /** 集計済みの行の読み込み元(定期の集計で全媒体の問い合わせ結果を媒体ごとに受け渡し、daily は since 以降の行) */
32
17
  export declare const fixedSource: (rows: readonly DailyRow[]) => Source;
33
18
  /** 種類を付け替えた読み込み元(renames は旧の種類 → 今の種類、付け替えで重なった行は閲覧数を合計) */
34
19
  export declare function renamedSource(source: Source, renames: Readonly<Record<string, string>>): Source;
35
- export {};
@@ -0,0 +1,24 @@
1
+ /** パスから種類と ID への対応(利用側が定義、例: /articles/<id> は記事) */
2
+ export type Classify = (path: string) => {
3
+ kind: string;
4
+ id: string;
5
+ };
6
+ /** 閲覧の出来事(project は媒体の識別子) */
7
+ export type ViewEvent = Readonly<{
8
+ kind: string;
9
+ id: string;
10
+ path: string;
11
+ date: string;
12
+ event: 'view';
13
+ project?: string;
14
+ }>;
15
+ /** 位置で持つ記録先の列の順 */
16
+ export declare const VIEW_COLUMNS: readonly ["kind", "id", "path", "date", "event", "project"];
17
+ /** 出来事にする設定(project は媒体の識別子) */
18
+ export type ViewOptions = Readonly<{
19
+ classify: Classify;
20
+ timeZone: string;
21
+ project?: string;
22
+ }>;
23
+ /** 閲覧の出来事(対象: GET で応答が200の HTML・ボット以外、ほかは null) */
24
+ export declare function viewOf(request: Request, response: Response, { classify, timeZone, project }: ViewOptions): ViewEvent | null;
@@ -9,7 +9,7 @@ export { type CallApi, type ConnectionPoint, type ContentTypePoint, createModule
9
9
  export { applyFieldRules, autoFieldsOf, type FieldRule, hiddenFieldsOf, restrictedChoicesOf, restrictedProblemsOf } from './fieldRules.js';
10
10
  export { pendingIds, pendingValue } from './pending.js';
11
11
  export { type AccountLink, type Action, ACTION_LEVELS, ACTIONS, allows, ANY_TARGET, canChangeRow, canDeleteContent, canEditContent, canPublishContent, canReadContent, canReadRow, canSubmitContent, checkPermissions, type ContentAccess, type Level, LEVELS, levelOf, ownsContent, ownsRow, type Permissions, type RowAccess } from './permissions.js';
12
- export { ADMIN_PERMISSIONS, ADMIN_ROLE, defaultRoleOf, isAdmin, rolePermissionsOf, type Role, SYSTEM_ROLES, type User, WRITER_ROLE } from './roles.js';
12
+ export { ADMIN_PERMISSIONS, ADMIN_ROLE, defaultRoleOf, isAdmin, rolePermissionsOf, type Role, SYSTEM_ROLES, type User, DEFAULT_ROLE } from './roles.js';
13
13
  export { isServerPath, OPS_PATHS, SERVER_PATHS } from './paths.js';
14
14
  export { CONFIG_FORMAT, type ConfigBundle, type ConfigPreview } from './transfer.js';
15
15
  export { IMPORT_ITEMS_MAX, IMPORT_PROBLEM_LABELS, IMPORT_PROBLEMS, type ImportIdChange, type ImportIdConflict, importIdOf, type ImportIdPlan, type ImportImage, type ImportItem, type ImportItemReport, type ImportProblem, type ImportProblemCode, type ImportReport, importReportOf, mergeImportReports, type MicroCmsApiInput, type MicroCmsContentResult, type MicroCmsDropped, type MicroCmsNote, planImportIds, sourceOf } from './import.js';
@@ -1,15 +1,15 @@
1
1
  import { type AccountLink, type Permissions } from './permissions.js';
2
2
  /** 管理者のロールの ID */
3
3
  export declare const ADMIN_ROLE = "admin";
4
- /** ライターのロールの ID(あれば新しいアカウントの既定、削除できる) */
5
- export declare const WRITER_ROLE = "writer";
4
+ /** 新しいアカウントの既定のロールの ID(テンプレートのロールの ID、あれば既定、削除できる) */
5
+ export declare const DEFAULT_ROLE = "writer";
6
6
  /** 管理者の権限の表(すべての対象・操作が可、DB の値より優先) */
7
7
  export declare const ADMIN_PERMISSIONS: Permissions;
8
8
  /** ロールの権限の表(管理者のロールは固定の表、ほかは保存した表) */
9
9
  export declare const rolePermissionsOf: (roleId: string, stored: Permissions) => Permissions;
10
10
  /** 削除できないロールの ID */
11
11
  export declare const SYSTEM_ROLES: readonly string[];
12
- /** 新しいアカウントの既定のロール(ライター、なければ管理者以外の最初のロール、それもなければ管理者) */
12
+ /** 新しいアカウントの既定のロール(DEFAULT_ROLE、なければ管理者以外の最初のロール、それもなければ管理者) */
13
13
  export declare const defaultRoleOf: (roles: readonly Pick<Role, "id">[]) => string;
14
14
  /** ロール(API /api/roles の行) */
15
15
  export type Role = Readonly<{
@@ -2,11 +2,12 @@ export { fieldZod, jsonSchemaOf, normalizeValues, zodOf, type ValueOptions } fro
2
2
  export { assetRefSchema, cropSchema, hotspotSchema, mediaSchema, type Asset, type AssetRef, type Crop, type DeliveredAsset, type Hotspot, type LocalImage, type Media } from './asset.js';
3
3
  export { coverPosition, coverStart, cropRect, croppedHotspot, objectPosition, stillOf, type CropRect } from './image.js';
4
4
  export { IMAGE_TYPE, VIDEO_TYPE } from './media.js';
5
- export { IMAGE_SIZE_MAX, imageParamsOf, MEDIA_SIGNING_KEY_BLOB, mediaBlobKeyOf, mediaExtOf, mediaKindOf, mediaMimeOf, mediaPathOf, parseMediaPath, signMediaUrl, verifyMediaUrl, withImageParams, type ImageParams, type AssetKind, type MediaPath } from './mediaUrl.js';
5
+ export { IMAGE_SIZE_MAX, imageParamsOf, isMediaId, MEDIA_SIGNING_KEY_BLOB, mediaBlobKeyOf, mediaExtOf, mediaKindOf, mediaMimeOf, mediaPathOf, parseMediaPath, signMediaUrl, verifyMediaUrl, withImageParams, type ImageParams, type AssetKind, type MediaPath } from './mediaUrl.js';
6
6
  export { diffSchema, renameType, type ChangeKind, type ChangeLevel, type DiffOptions, type SchemaChange } from './changes.js';
7
7
  export { dateRoleFieldsOf, primaryTypeOf, requiredPrimaryTypeOf, SOURCES_FIELD, sourceIdsOf, sourcesFieldOf, sourceTypeOf, type SourcesField } from './capabilities.js';
8
8
  export { appliesTo, checkFields, fieldsFor } from './check.js';
9
9
  export { deliveredFields, emptyValues, pickValues, referencedIds, referencesOf, valueFieldsOf } from './values.js';
10
+ export { typesSourceOf } from './typesSource.js';
10
11
  export { entryRefsOf, type EntryRef } from './refs.js';
11
12
  export { acceptsRole, bodyFieldsOf, computedFieldsOf, FIELD_ROLES, idInputOf, initialValueOf, isPublishable, isInputField, isListType, isMediaType, isSingleton, isStoredType, MEDIA_TYPE_IDS, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, resolvedRolesOf, roleFieldOf, toneOf, typeOf, type ComputedKind, type FieldRole, type FieldRoles, type FieldComputed, type ContentFormat, type ContentModel, type ContentType, type FieldCondition, type FieldDef, type FieldDerive, type FieldFixedBy, type CountDef, type FieldProblem, type MediaKind, type OptionTone, type Publishing, type ReferenceRules, type SelectOption, usesReview } from './model.js';
12
13
  export { hasPublicPage, imageFrameOf, pagePathOf, publicPathOf, withPagesFrom, type ImageFrame, type ImageFrameDef, type PageDef, type PageEntry, type PageRule } from './page.js';
@@ -16,6 +16,8 @@ export type MediaPath = Readonly<{
16
16
  width?: number;
17
17
  height?: number;
18
18
  }>;
19
+ /** 実体の ID の形か(CMS の /media と配信の Worker の共通、保存先のほかのキーを ID として読ませない) */
20
+ export declare const isMediaId: (id: string) => boolean;
19
21
  /** 配信 URL のパス(媒体・ID の形が不正、配信しない形式なら null) */
20
22
  export declare function mediaPathOf({ kind, project, id, mimeType, width, height }: Readonly<{
21
23
  kind: AssetKind;
@@ -0,0 +1,7 @@
1
+ import type { ContentModel } from './model.js';
2
+ /**
3
+ * スキーマから配信 API の値の型の定義(TypeScript のモジュールの文)
4
+ * - 型ごとに `export type <PascalCase の型の id>`、全体の対応 `CmsTypes` と型の id の一覧 `TYPE_IDS`
5
+ * - types: 配信する型だけ(メディアの型を除いた一覧、配信 API の `/types` の値)
6
+ */
7
+ export declare function typesSourceOf(model: Pick<ContentModel, 'types'>): string;
@@ -1,2 +1,12 @@
1
- export { recordView } from '../../analytics/src/index.js';
2
- export type { Classify, Dataset } from '../../analytics/src/index.js';
1
+ import { type Classify, type ViewOptions } from '../../analytics/src/index.js';
2
+ export type { Classify };
3
+ /** Analytics Engine のデータセットの binding(書き込みだけ) */
4
+ export type Dataset = {
5
+ writeDataPoint(point: {
6
+ blobs?: string[];
7
+ doubles?: number[];
8
+ indexes?: string[];
9
+ }): void;
10
+ };
11
+ /** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、binding なしと記録の失敗は無視) */
12
+ export declare function recordView(dataset: Dataset | undefined, request: Request, response: Response, options: ViewOptions): void;
@@ -1,4 +1,4 @@
1
- export { coverPosition, coverStart, createClock, cropRect, croppedHotspot, IMAGE_TYPE, isDateOnly, mediaSchema, objectPosition, pagePathOf, parseModel, parseScreens, publicPathOf, stillOf, typeOf, VIDEO_TYPE } from '../../model/src/index.js';
1
+ export { coverPosition, coverStart, createClock, cropRect, croppedHotspot, IMAGE_TYPE, isDateOnly, mediaSchema, objectPosition, pagePathOf, parseModel, parseScreens, publicPathOf, stillOf, typeOf, typesSourceOf, VIDEO_TYPE } from '../../model/src/index.js';
2
2
  export type { Asset, AssetRef, Clock, ContentModel, ContentType, Crop, CropRect, DateString, FieldDef, Hotspot, LocalImage, Media, PageDef, PageEntry, ScreenSettings } from '../../model/src/index.js';
3
3
  export { CONFIG_FORMAT } from '../../cms-contract/src/index.js';
4
4
  export type { ConfigBundle } from '../../cms-contract/src/index.js';
@@ -0,0 +1,20 @@
1
+ import { type ContentType } from '../../model/src/index.js';
2
+ /**
3
+ * maezae-types の終了の状態
4
+ * - drift: --check でファイルが CMS のスキーマと違う
5
+ * - failed: 引数と鍵の不足、CMS の拒否(4xx)、応答の形の誤り、ファイルの読み書きの失敗
6
+ * - unreachable: CMS に届かない(接続の失敗、5xx)
7
+ */
8
+ export declare const TYPES_EXIT_CODES: {
9
+ readonly ok: 0;
10
+ readonly drift: 1;
11
+ readonly failed: 2;
12
+ readonly unreachable: 3;
13
+ };
14
+ /**
15
+ * 配信 API の型の一覧(/types、表示名・選択肢・上限・ページの URL の形を含む。実行時に読む値もここから)
16
+ * - 例外: 2xx でなければ `fetch_failed: <状態> <本文>`、本文に types がなければ `invalid_response`、接続の失敗は fetcher の例外のまま
17
+ */
18
+ export declare function fetchDeliveredTypes(url: string, apiKey: string, fetcher?: typeof fetch): Promise<readonly ContentType[]>;
19
+ /** CLI の本体(戻り値は終了の状態 TYPES_EXIT_CODES、失敗は例外にせず標準エラーと終了の状態で返す) */
20
+ export declare function runTypesCli(argv: readonly string[], env: Readonly<Record<string, string | undefined>>, fetcher?: typeof fetch): Promise<number>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1 @@
1
+ export * from './types/sdk/src/types-cli.js';
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { r as runTypesCli } from "./schema-CUrLZWsb.js";
3
+ // src/types-cli.ts
4
+ process.exitCode = await runTypesCli(process.argv.slice(2), process.env);
@@ -0,0 +1,64 @@
1
+ // ../model/src/values.ts
2
+ /** 配信する項目(非公開の項目を除く、まとまりの中も) */
3
+ const deliveredFields = (fields) => fields.filter((f) => !f.private).map((f) => f.type === "group" ? {
4
+ ...f,
5
+ fields: deliveredFields(f.fields)
6
+ } : f);
7
+ // ../model/src/typesSource.ts
8
+ /** 生成した型の定義が SDK から読む型の import の元 */
9
+ const SDK = "@ryusenkei/maezae";
10
+ /** 型の import の行(指定は JSON の文字列の形) */
11
+ const importLine = (names, spec) => `import type { ${names.join(", ")} } from ${JSON.stringify(spec)};`;
12
+ /** プロパティの名前(識別子でない key は JSON の文字列の形) */
13
+ const propertyName = (key) => /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
14
+ const pascal = (id) => id.replace(/(^|[-_])(\w)/g, (_, __, ch) => ch.toUpperCase());
15
+ /** 項目の1つ分の値の型(SDK の SingleValue と同じ) */
16
+ function singleOf(f, indent) {
17
+ switch (f.type) {
18
+ case "text":
19
+ case "textarea":
20
+ case "datetime":
21
+ case "reference": return "string";
22
+ case "date": return "DateString";
23
+ case "number": return "number";
24
+ case "checkbox": return "boolean";
25
+ case "select": return f.options.map((o) => JSON.stringify(o.value)).join(" | ") || "string";
26
+ case "media": return "DeliveredAsset | null";
27
+ case "body": return "Body<DeliveredAsset | null>";
28
+ case "group": return `{\n${fieldsOf(deliveredFields(f.fields), `${indent} `)}\n${indent}}`;
29
+ }
30
+ }
31
+ /** 項目の値の型(SDK の FieldValue と同じ、複数は readonly の配列、値なしは null) */
32
+ const valueOf = (f, indent) => {
33
+ const one = singleOf(f, indent);
34
+ if ("multiple" in f && f.multiple) return `readonly ${one.includes(" | ") ? `(${one})` : one}[] | null`;
35
+ return one.endsWith(" | null") ? one : `${one} | null`;
36
+ };
37
+ /** 配信する項目の種類(まとまりの中を含む) */
38
+ const kindsOf = (fields) => deliveredFields(fields).flatMap((f) => [f.type, ...f.type === "group" ? kindsOf(f.fields) : []]);
39
+ const fieldsOf = (fields, indent) => fields.map((f) => `${indent}readonly ${propertyName(f.key)}: ${valueOf(f, indent)};`).join("\n");
40
+ const typeOf = (t) => `export type ${pascal(t.id)} = Readonly<{\n id: string;\n _meta: ItemMeta;\n${fieldsOf(deliveredFields(t.fields), " ")}\n}>;`;
41
+ /**
42
+ * スキーマから配信 API の値の型の定義(TypeScript のモジュールの文)
43
+ * - 型ごとに `export type <PascalCase の型の id>`、全体の対応 `CmsTypes` と型の id の一覧 `TYPE_IDS`
44
+ * - types: 配信する型だけ(メディアの型を除いた一覧、配信 API の `/types` の値)
45
+ */
46
+ function typesSourceOf(model) {
47
+ const types = [...model.types].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
48
+ const kinds = new Set(types.flatMap((t) => kindsOf(t.fields)));
49
+ const uses = (kind) => kinds.has(kind);
50
+ const delivery = ["ItemMeta", ...uses("media") || uses("body") ? ["DeliveredAsset"] : []].sort();
51
+ return [
52
+ "// Generated from the CMS schema (GET /content/v1/types). Do not edit; regenerate with maezae-types.",
53
+ importLine(delivery, `${SDK}/delivery`),
54
+ ...uses("body") ? [importLine(["Body"], `${SDK}/body`)] : [],
55
+ ...uses("date") ? [importLine(["DateString"], `${SDK}/model`)] : [],
56
+ "",
57
+ ...types.flatMap((t) => [typeOf(t), ""]),
58
+ `export type CmsTypes = {\n${types.map((t) => ` ${JSON.stringify(t.id)}: ${pascal(t.id)};`).join("\n")}\n};`,
59
+ "",
60
+ `export const TYPE_IDS = [${types.map((t) => JSON.stringify(t.id)).join(", ")}] as const;`,
61
+ ""
62
+ ].join("\n");
63
+ }
64
+ export { typesSourceOf as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",
@@ -43,8 +43,15 @@
43
43
  "./dev": {
44
44
  "types": "./dist/dev.d.ts",
45
45
  "default": "./dist/dev.js"
46
+ },
47
+ "./schema": {
48
+ "types": "./dist/schema.d.ts",
49
+ "default": "./dist/schema.js"
46
50
  }
47
51
  },
52
+ "bin": {
53
+ "maezae-types": "./dist/types-cli.js"
54
+ },
48
55
  "files": [
49
56
  "dist"
50
57
  ],
@@ -1,22 +0,0 @@
1
- /** Analytics Engine のデータセットの binding(書き込みだけ) */
2
- export type Dataset = {
3
- writeDataPoint(point: {
4
- blobs?: string[];
5
- doubles?: number[];
6
- indexes?: string[];
7
- }): void;
8
- };
9
- /** パスから種類と ID への対応(利用側が定義、例: /articles/<id> は記事) */
10
- export type Classify = (path: string) => {
11
- kind: string;
12
- id: string;
13
- };
14
- /** 記録の設定(project は媒体の識別子) */
15
- type RecordOptions = {
16
- classify: Classify;
17
- timeZone: string;
18
- project?: string;
19
- };
20
- /** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、記録の失敗は無視) */
21
- export declare function recordView(dataset: Dataset | undefined, request: Request, response: Response, { classify, timeZone, project }: RecordOptions): void;
22
- export {};