@ryusenkei/maezae 0.14.0 → 0.16.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
@@ -97,6 +97,37 @@ const cards = await client.page<Card>('/post', { limit: 20, fields, filter: { ti
97
97
  const one = await client.find<Card>('/post/p1', listParams({ fields }));
98
98
  ```
99
99
 
100
+ ## 変更の一覧(0.16.0 から)
101
+
102
+ `GET /content/v1/changes?since=<cursor>&limit=N` は、型をまたいだ行の変更を順に返す。検索や静的なビルドのために導入先が自分の側へ写すときの土台で、Webhook の取りこぼし(再送は約55時間半で止まる)もここで回収する
103
+
104
+ ```json
105
+ { "changes": [{ "type": "post", "id": "p1", "change": "upsert", "event": "published", "at": "2026-11-12T00:00:00.000Z" }], "next": "…", "more": false }
106
+ ```
107
+
108
+ - `since` なしは初回の全件の取得。公開中の全行を `event: "snapshot"` で返し、終わったら記録の続きに移る
109
+ - `change` は取得した時点の状態。`upsert` の行は配信 API で取り直し、`delete` の行は手元から消す。同じ行が何度も現れるので、行ごとに最後の状態で上書きする
110
+ - `event` は記録の出来事(`created`・`updated`・`deleted`・`published`・`unpublished`)。予約の公開は、公開日時を過ぎてから `published` で現れる
111
+ - `next` は変更がなくても返る。保存して次回の `since` に渡す。`more` が `true` ならすぐに続きを読む
112
+ - `limit` は1〜1000(既定は100)
113
+ - 記録は30日で消える。それより古い `since` は410(`code: "cursor_expired"`)を返すので、`since` なしで取り直す
114
+ - 公開の管理がある型の下書きの行の編集と、メディアは記録しない
115
+
116
+ `readChanges` は `more` がなくなるまで読み、ページごとに `apply` を呼んで次回の `since` を返す
117
+
118
+ ```ts
119
+ import { createDeliveryClient, DeliveryError, readChanges } from '@ryusenkei/maezae/delivery';
120
+
121
+ try {
122
+ since = await readChanges(client, since, async ({ changes }) => {
123
+ for (const c of changes) c.change === 'upsert' ? await upsert(c.type, c.id) : await remove(c.type, c.id);
124
+ });
125
+ } catch (e) {
126
+ if (e instanceof DeliveryError && e.status === 410) since = null; // 次回は全件から
127
+ else throw e;
128
+ }
129
+ ```
130
+
100
131
  ## 回数の制限
101
132
 
102
133
  - 対象は公開のエンドポイント(`https://api.maezae.app/<媒体>/content/v1/…`)だけ。上限は媒体と API の鍵ごとに1分で600回で、データセンターごとの近似で数える
@@ -147,6 +178,15 @@ if (!(await verifyRequest(env.CMS_FUNCTION_SECRET, request, { header: SIGNED_HEA
147
178
  const body = (await request.json()) as PublishCheckRequest;
148
179
  ```
149
180
 
181
+ - 検査だけの実行: 提出・公開前の検査(`publish_check`)の本文の `dryRun` が `true` なら、提出・公開をせずに結果だけを見る依頼(MCP の `check_entry` など、0.15.0)
182
+ - 行は書きかけでもよい。返した `problems` は提出・公開を止めず、人と AI に見せるだけ
183
+ - 重い検査(引用の照合など)を `dryRun` のときだけ足してよい。時間切れ(1件3秒)は同じ
184
+ - `problems` の `message` はそのまま AI と画面に出るため、秘密の値・内部の URL を含めない
185
+
186
+ ```ts
187
+ if (body.hook === 'publish_check' && body.dryRun) problems.push(...(await verifyQuotes(body.values)));
188
+ ```
189
+
150
190
  - Webhook の重複: 失敗した送信は同じ本文で再送する。本文の `id`(イベントの ID)を処理済みとして保存し、同じ `id` は処理せずに 2xx を返す
151
191
 
152
192
  ## Webhook の本文と契約の版
package/dist/delivery.js CHANGED
@@ -91,6 +91,21 @@ function memoryDeliveryCache(maxEntries = 500) {
91
91
  }
92
92
  };
93
93
  }
94
+ // src/delivery/changes.ts
95
+ /**
96
+ * since から more がなくなるまで読み、ページごとに apply を呼ぶ(戻り値は次回の since)
97
+ * - apply が成功してから次のページへ進む(失敗したら例外で止まり、保存済みの since から再開できる)
98
+ * - 410(保存の期間を過ぎた cursor): DeliveryError、since を null にして全件から取り直す
99
+ */
100
+ async function readChanges(client, since, apply, { limit } = {}) {
101
+ let cursor = since;
102
+ for (;;) {
103
+ const page = await client.changes(cursor, limit === void 0 ? {} : { limit });
104
+ await apply(page);
105
+ cursor = page.next;
106
+ if (!page.more) return cursor;
107
+ }
108
+ }
94
109
  // src/delivery/query.ts
95
110
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
96
111
  const OPS = [
@@ -211,11 +226,20 @@ function createDeliveryClient(delivery, { drafts = false, at } = {}) {
211
226
  partial: res.headers.has("x-partial")
212
227
  };
213
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
+ });
214
237
  return {
215
238
  find,
216
239
  get,
217
240
  page,
218
- prefetch
241
+ prefetch,
242
+ changes
219
243
  };
220
244
  }
221
245
  // ../model/src/mediaUrl.ts
@@ -408,4 +432,4 @@ function createDeliveryClock(timeZone) {
408
432
  })
409
433
  };
410
434
  }
411
- export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
435
+ export { DELIVERY_PATH, DeliveryError, cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, subPath, withImageParams };
@@ -24,6 +24,6 @@ export type HandoffLoginOptions = Readonly<{
24
24
  export declare function handoffLogin({ secret, loginUrl, consume, basePath, sessionTtlSec }: HandoffLoginOptions): LoginAuth;
25
25
  /**
26
26
  * 2つのログインを順に試す(emailOf は first が null なら second、入り口とログアウトは first)
27
- * - 用途: 引き継ぎのログインへの移行の間、Access の JWT も受け付ける
27
+ * - 用途: 引き継ぎのログインに加えて、運営者のログイン(apps/api の operator.ts)を受け付ける
28
28
  */
29
29
  export declare function eitherLogin(first: LoginAuth, second: LoginAuth): LoginAuth;
@@ -40,6 +40,7 @@ export declare const ERROR_STATUS: {
40
40
  readonly article_editing: 409;
41
41
  readonly invalid_state: 409;
42
42
  readonly unpublish_first: 409;
43
+ readonly cursor_expired: 410;
43
44
  readonly body_too_large: 413;
44
45
  readonly rate_limited: 429;
45
46
  readonly upstream_failed: 502;
@@ -48,12 +48,17 @@ export type SaveCheckResponse = Readonly<{
48
48
  }>[];
49
49
  values?: Readonly<Record<string, unknown>>;
50
50
  }>;
51
- /** 提出・公開前の検査の本文(stage は submit か publish、values は保存済みの行の値、id はテストの呼び出しだけ null) */
51
+ /**
52
+ * 提出・公開前の検査の本文
53
+ * - stage: submit か publish(検査だけの実行は次に進む段階)、values: 保存済みの行の値、id: テストの呼び出しだけ null
54
+ * - dryRun: 検査だけの実行なら true(提出・公開の点検では省略)
55
+ */
52
56
  export type PublishCheckRequest = Readonly<{
53
57
  hook: 'publish_check';
54
58
  type: string;
55
59
  id: string | null;
56
60
  stage: 'submit' | 'publish';
61
+ dryRun?: true;
57
62
  values: Readonly<Record<string, unknown>>;
58
63
  user: Readonly<{
59
64
  id: string;
@@ -0,0 +1,23 @@
1
+ import type { DeliveryClient } from './client.js';
2
+ /** 変更の一覧の1件(at は配信 API に現れた日時、`ISO 8601` の UTC) */
3
+ export type Change = Readonly<{
4
+ type: string;
5
+ id: string;
6
+ change: 'upsert' | 'delete';
7
+ event: 'created' | 'updated' | 'deleted' | 'published' | 'unpublished' | 'snapshot';
8
+ at: string;
9
+ }>;
10
+ /** 変更の一覧の1ページ */
11
+ export type ChangesPage = Readonly<{
12
+ changes: readonly Change[];
13
+ next: string;
14
+ more: boolean;
15
+ }>;
16
+ /**
17
+ * since から more がなくなるまで読み、ページごとに apply を呼ぶ(戻り値は次回の since)
18
+ * - apply が成功してから次のページへ進む(失敗したら例外で止まり、保存済みの since から再開できる)
19
+ * - 410(保存の期間を過ぎた cursor): DeliveryError、since を null にして全件から取り直す
20
+ */
21
+ export declare function readChanges(client: Pick<DeliveryClient, 'changes'>, since: string | null, apply: (page: ChangesPage) => Promise<void> | void, { limit }?: {
22
+ limit?: number;
23
+ }): Promise<string>;
@@ -1,5 +1,6 @@
1
1
  import type { ContentType } from '../../../model/src/index.js';
2
2
  import { type ListQuery, type Page } from './query.js';
3
+ import type { ChangesPage } from './changes.js';
3
4
  /** 配信 API のパスの前半 */
4
5
  export declare const DELIVERY_PATH = "/content/v1";
5
6
  /** 配信 API への接続(path は /content/v1 から始まるパスとクエリ) */
@@ -57,6 +58,9 @@ export declare function createDeliveryClient(delivery: Delivery, { drafts, at }?
57
58
  prefetch: (paths: readonly string[], { ifNoneMatch }?: {
58
59
  ifNoneMatch?: string;
59
60
  }) => Promise<Prefetched>;
61
+ changes: (since: string | null, { limit }?: {
62
+ limit?: number;
63
+ }) => Promise<ChangesPage>;
60
64
  };
61
65
  /** 配信 API の読み取り(createDeliveryClient の戻り値) */
62
66
  export type DeliveryClient = ReturnType<typeof createDeliveryClient>;
@@ -1,4 +1,5 @@
1
1
  export { cachedDelivery, memoryDeliveryCache, type DeliveryCache } from './cache.js';
2
+ export { readChanges, type Change, type ChangesPage } from './changes.js';
2
3
  export { createDeliveryClient, DELIVERY_PATH, DeliveryError, subPath, type BundleResult, type Delivery, type DeliveryClient, type Prefetched } from './client.js';
3
4
  export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media.js';
4
5
  export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../../model/src/index.js';
@@ -1,2 +1,2 @@
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';
1
+ export { cachedDelivery, createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, memoryDeliveryCache, optionLabels, pagePath, parseMediaPath, readChanges, subPath, withImageParams } from './delivery/index.js';
2
+ export type { BundleResult, Change, ChangesPage, DeliveredAsset, Delivery, DeliveryCache, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, FieldPath, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched, Selected } from './delivery/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",