@ryusenkei/maezae 0.6.0 → 0.7.1

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
@@ -19,6 +19,8 @@ maezae の SDK(導入先のサイト向け)
19
19
  - バインディング: `services: [{ "binding": "CMS", "service": "<CMS のサーバーの Worker>", "entrypoint": "Delivery" }]`
20
20
  - 媒体: URL のホスト名(媒体の管理画面のホスト名)
21
21
  - 鍵: 媒体の管理画面の「設定 → 連携」で発行した API の鍵
22
+ - 1件の形: `{ id, ...項目, _meta: { created_at, updated_at, published_at } }`、published_at は公開の管理がある型だけ(0.7.0 から)
23
+ - 一覧: 本文の種類の項目を含めない、本文は1件の取得か `/bundle` で
22
24
 
23
25
  ```ts
24
26
  import { createDeliveryClient } from '@ryusenkei/maezae/delivery';
package/dist/delivery.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as croppedHotspot, d as createClock, i as typeOf, r as optionsOf, s as cropRect, t as pagePathOf } from "./page-UEKb7_gu.js";
1
+ import { c as croppedHotspot, d as createClock, i as typeOf, r as optionsOf, s as cropRect, t as pagePathOf } from "./page-TFUlKv3R.js";
2
2
  // ../delivery-client/src/query.ts
3
3
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  /** in の値の検査(コンマを含む値はクエリで区切れない) */
@@ -277,10 +277,9 @@ function mediaOf(base, imageUrl) {
277
277
  // ../delivery-client/src/schema.ts
278
278
  /** 選択肢の項目から表示名の表(値 → 表示名、key はまとまりの中なら `links.kind` の形、選択肢の項目でなければ空) */
279
279
  const optionLabels = (schema, type, key) => Object.fromEntries(optionsOf(typeOf(schema, type), key).map((o) => [o.value, o.label]));
280
- /** 型の項目(コアの項目と追加の項目から、なければ例外: 導入先の設定の誤り) */
280
+ /** 型の項目(なければ例外: 導入先の設定の誤り) */
281
281
  function fieldOf(schema, type, key) {
282
- const t = typeOf(schema, type);
283
- const field = [...t.core ?? [], ...t.fields].find((f) => f.key === key);
282
+ const field = typeOf(schema, type).fields.find((f) => f.key === key);
284
283
  if (!field) throw new Error(`型 ${type} に項目 ${key} がない`);
285
284
  return field;
286
285
  }
package/dist/model.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as coverPosition, c as croppedHotspot, d as createClock, f as isDateOnly, i as typeOf, l as objectPosition, n as publicPathOf, o as coverStart, s as cropRect, t as pagePathOf, u as stillOf } from "./page-UEKb7_gu.js";
1
+ import { a as coverPosition, c as croppedHotspot, d as createClock, f as isDateOnly, i as typeOf, l as objectPosition, n as publicPathOf, o as coverStart, s as cropRect, t as pagePathOf, u as stillOf } from "./page-TFUlKv3R.js";
2
2
  import { i as CORE_COMPONENTS, t as declaredComponentsSchema } from "./declared-9_jI3JXH.js";
3
3
  import "./src-BrLwIWaT.js";
4
4
  import { z } from "zod";
@@ -72,14 +72,13 @@ const IMAGE_TYPE = {
72
72
  id: "image",
73
73
  label: "画像",
74
74
  description: "見せたい点(hotspot)と切り抜き(crop)を画像ごとに1つ保持、切り抜く場所すべてで共通",
75
- core: file,
76
- fields: []
75
+ fields: file
77
76
  };
78
77
  /** 動画の型(配信と再生は最初に使用する場所が決まったときに実装) */
79
78
  const VIDEO_TYPE = {
80
79
  id: "video",
81
80
  label: "動画",
82
- core: [
81
+ fields: [
83
82
  ...file,
84
83
  {
85
84
  key: "duration",
@@ -94,30 +93,101 @@ const VIDEO_TYPE = {
94
93
  accept: ["image"],
95
94
  description: "読み込み前・動きを抑える設定・OG で代わりに表示"
96
95
  }
97
- ],
98
- fields: []
96
+ ]
99
97
  };
100
98
  // ../model/src/capabilities.ts
101
- const NONE = {
102
- editorial: false,
103
- derived: false,
104
- sources: false,
105
- shared: false
99
+ /** 出典の項目と、内側の項目の key */
100
+ const SOURCES_FIELD = {
101
+ key: "sources",
102
+ source: "source",
103
+ note: "note"
106
104
  };
107
- const CORE_TABLES = {
108
- articles: {
109
- ...NONE,
110
- editorial: true,
111
- derived: true,
112
- sources: true
105
+ // ../model/src/legacy.ts
106
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
107
+ /** 記事の表の型だった目印(古いバージョンの table の値) */
108
+ const ARTICLES = "articles";
109
+ const PUBLISHED_AT = "published_at";
110
+ /** 本文から作成する値の計算の項目(from は本文の項目の key) */
111
+ const computedFields = (from) => [
112
+ {
113
+ key: "excerpt",
114
+ label: "抜粋",
115
+ type: "textarea",
116
+ computed: {
117
+ kind: "excerpt",
118
+ from
119
+ }
120
+ },
121
+ {
122
+ key: "read_min",
123
+ label: "読了の分",
124
+ type: "number",
125
+ computed: {
126
+ kind: "read_min",
127
+ from
128
+ }
113
129
  },
114
- sources: {
115
- ...NONE,
116
- shared: true
130
+ {
131
+ key: "links",
132
+ label: "リンク先",
133
+ type: "text",
134
+ multiple: true,
135
+ computed: {
136
+ kind: "links",
137
+ from
138
+ }
117
139
  }
118
- };
119
- /** 表の性質(コアの表にない表はすべてなし) */
120
- const tableTraitsOf = (table) => table && Object.hasOwn(CORE_TABLES, table) ? CORE_TABLES[table] : NONE;
140
+ ];
141
+ /** 出典の参照の項目を複数のまとまりに */
142
+ const sourcesGroup = (f) => ({
143
+ key: f.key,
144
+ label: f.label,
145
+ ...f.private !== void 0 && { private: f.private },
146
+ type: "group",
147
+ multiple: true,
148
+ fields: [{
149
+ key: SOURCES_FIELD.source,
150
+ label: f.label,
151
+ type: "reference",
152
+ to: f.to
153
+ }, {
154
+ key: SOURCES_FIELD.note,
155
+ label: "注記",
156
+ type: "textarea"
157
+ }]
158
+ });
159
+ /** 古い形の型を今の形に(core・table のない型はそのまま) */
160
+ function normalizeType(t) {
161
+ if (!("core" in t) && !("table" in t)) return t;
162
+ const { core, table, ...rest } = t;
163
+ const articles = table === ARTICLES;
164
+ const coreFields = (Array.isArray(core) ? core : []).filter(isRecord).flatMap((f) => {
165
+ if (articles && f.key === PUBLISHED_AT) return [];
166
+ if (articles && f.key === SOURCES_FIELD.key && f.type === "reference") return [sourcesGroup(f)];
167
+ return [f];
168
+ });
169
+ const keys = new Set(coreFields.map((f) => f.key));
170
+ const fields = [...coreFields, ...(Array.isArray(t.fields) ? t.fields : []).filter((f) => !isRecord(f) || !keys.has(f.key))];
171
+ if (!articles) return {
172
+ ...rest,
173
+ fields
174
+ };
175
+ const body = fields.find((f) => isRecord(f) && f.type === "body");
176
+ const present = new Set(fields.filter(isRecord).map((f) => f.key));
177
+ const computed = body ? computedFields(String(body.key)).filter((f) => !present.has(f.key)) : [];
178
+ return {
179
+ ...rest,
180
+ fields: [...fields, ...computed]
181
+ };
182
+ }
183
+ /** 古い形のスキーマの JSON を今の形に(形の分からない値はそのまま、検査は parseModel) */
184
+ function normalizeLegacyModel(json) {
185
+ if (!isRecord(json) || !Array.isArray(json.types)) return json;
186
+ return {
187
+ ...json,
188
+ types: json.types.map((t) => isRecord(t) ? normalizeType(t) : t)
189
+ };
190
+ }
121
191
  // ../model/src/schema.ts
122
192
  const key = z.string().regex(/^[a-z][a-z0-9_]*$/, "id は英小文字で始まり、英小文字・数字・アンダーラインだけ");
123
193
  const fieldKey = z.string().regex(/^[a-z][a-zA-Z0-9_-]*$/, "key は英小文字で始まり、英字・数字・アンダーライン・ハイフンだけ");
@@ -125,6 +195,14 @@ const condition = z.object({
125
195
  key: fieldKey,
126
196
  in: z.array(z.string().min(1)).min(1)
127
197
  });
198
+ const computed = z.object({
199
+ kind: z.enum([
200
+ "excerpt",
201
+ "read_min",
202
+ "links"
203
+ ]),
204
+ from: fieldKey
205
+ });
128
206
  const base = {
129
207
  key: fieldKey,
130
208
  label: z.string().min(1),
@@ -138,7 +216,8 @@ const base = {
138
216
  when: condition.optional(),
139
217
  private: z.boolean().optional(),
140
218
  adminOnly: z.boolean().optional(),
141
- pendingUntil: fieldKey.optional()
219
+ pendingUntil: fieldKey.optional(),
220
+ computed: computed.optional()
142
221
  };
143
222
  const frame = z.object({
144
223
  when: condition.optional(),
@@ -296,8 +375,7 @@ const nestedRules = (fields) => fields.flatMap((f) => (f.fields ?? []).flatMap((
296
375
  /**
297
376
  * システムの値と同じ名前で、項目の key に使用できない名前(API・配信が項目の値を上書きするため)
298
377
  * - すべての型: 行の ID・作成と更新の日時・作成者、管理画面の一覧で参照する行の数 uses
299
- * - 公開の管理がある型と、コアの表を持つ型の追加の項目: 編集の機能の列
300
- * - 本文から作成する列を持つ表の型: excerpt・read_min・links
378
+ * - 公開の管理がある型: 公開の機能の値
301
379
  * - 対象外: API・配信の値に出ない列 type、導入先の設定で決まるアカウントの email・account_role
302
380
  */
303
381
  const RESERVED = [
@@ -315,22 +393,25 @@ const RESERVED_EDITORIAL = [
315
393
  "review_note",
316
394
  "assignee"
317
395
  ];
318
- const RESERVED_DERIVED = [
319
- "excerpt",
320
- "read_min",
321
- "links"
322
- ];
323
- /** 予約の名前と同じ最上位の key(コアの項目は表の列そのものなので、編集の機能の列は対象外) */
396
+ /** 予約の名前と同じ最上位の key */
324
397
  function reservedKeysOf(t) {
325
- const derived = tableTraitsOf(t.table).derived ? RESERVED_DERIVED : [];
326
- const editorial = (t.publishing ?? "none") !== "none" || t.table ? RESERVED_EDITORIAL : [];
327
- const core = t.core ?? [];
328
- const extra = [...t.fields.map((f) => f.key), ...(t.counts ?? []).map((c) => c.key)];
329
- return [...core.map((f) => f.key).filter((k) => [...RESERVED, ...derived].includes(k)), ...extra.filter((k) => [
330
- ...RESERVED,
331
- ...derived,
332
- ...editorial
333
- ].includes(k))];
398
+ const reserved = [...RESERVED, ...(t.publishing ?? "none") !== "none" ? RESERVED_EDITORIAL : []];
399
+ return [...t.fields.map((f) => f.key), ...(t.counts ?? []).map((c) => c.key)].filter((k) => reserved.includes(k));
400
+ }
401
+ /** 計算の種類ごとに受け付ける項目の種類 */
402
+ const COMPUTED_TYPES = {
403
+ excerpt: (f) => (f.type === "text" || f.type === "textarea") && !f.multiple,
404
+ read_min: (f) => f.type === "number",
405
+ links: (f) => f.type === "text" && Boolean(f.multiple)
406
+ };
407
+ /** 計算の項目の誤り(最上位の項目と、まとまりの中にある計算の項目) */
408
+ function computedProblems(fields) {
409
+ const nested = (fs, path) => fs.flatMap((f) => [...f.computed ? [`${path}${f.key}: まとまりの中の項目は計算の項目なし`] : [], ...nested(f.fields ?? [], `${path}${f.key}.`)]);
410
+ return fields.flatMap((f) => {
411
+ const inner = nested(f.fields ?? [], `${f.key}.`);
412
+ if (!f.computed) return inner;
413
+ return [...[...fields.find((g) => g.key === f.computed.from)?.type === "body" ? [] : [`${f.key}: 計算の元 ${f.computed.from} が同じ型の本文の項目でない`], ...COMPUTED_TYPES[f.computed.kind]?.(f) ? [] : [`${f.key}: 計算の種類 ${f.computed.kind} に合わない項目の種類 ${f.type}`]], ...inner];
414
+ });
334
415
  }
335
416
  /** 本文の項目が使用する部品の名前(まとまりの中も、key は項目の key) */
336
417
  const bodyComponents = (fields) => fields.flatMap((f) => f.type === "body" ? f.profile.components.map((name) => ({
@@ -349,8 +430,6 @@ const contentType = z.object({
349
430
  "draft",
350
431
  "review"
351
432
  ]).optional(),
352
- table: key.optional(),
353
- core: z.array(field).optional(),
354
433
  fields: z.array(field),
355
434
  pages: z.array(page).optional(),
356
435
  counts: z.array(z.object({
@@ -360,29 +439,33 @@ const contentType = z.object({
360
439
  })).optional(),
361
440
  og: z.string().startsWith("/", "OG 画像の雛形は / で始まる").optional(),
362
441
  idInput: z.boolean().optional()
363
- }).refine((t) => t.format !== "object" || !t.core?.length, { message: "オブジェクト形式の型はコアの項目なし" }).refine((t) => new Set([...t.core ?? [], ...t.fields].map((f) => f.key)).size === (t.core?.length ?? 0) + t.fields.length, { message: "項目の key が重複(コアの項目を含む)" }).refine((t) => conditionsValid([...t.core ?? [], ...t.fields]), { message: "項目の条件が同じ階層の選択肢の項目と値を指していない" }).refine((t) => (t.pages ?? []).every((p) => conditionValid(p.when, [...t.core ?? [], ...t.fields])), { message: "ページの条件が型の選択肢の項目と値を指していない" }).superRefine((t, ctx) => {
442
+ }).refine((t) => new Set(t.fields.map((f) => f.key)).size === t.fields.length, { message: "項目の key が重複" }).refine((t) => conditionsValid(t.fields), { message: "項目の条件が同じ階層の選択肢の項目と値を指していない" }).refine((t) => (t.pages ?? []).every((p) => conditionValid(p.when, t.fields)), { message: "ページの条件が型の選択肢の項目と値を指していない" }).superRefine((t, ctx) => {
364
443
  for (const k of reservedKeysOf(t)) ctx.addIssue({
365
444
  code: "custom",
366
445
  message: `型 ${t.id} の key ${k} はシステムの値と同じ名前(別の key に)`
367
446
  });
368
- for (const k of nestedRules([...t.core ?? [], ...t.fields])) ctx.addIssue({
447
+ for (const k of nestedRules(t.fields)) ctx.addIssue({
369
448
  code: "custom",
370
449
  message: `型 ${t.id} の項目 ${k}: まとまりの中の項目は決まる値 derive・参照先で固定 fixedBy なし`
371
450
  });
451
+ for (const m of computedProblems(t.fields)) ctx.addIssue({
452
+ code: "custom",
453
+ message: `型 ${t.id} の項目 ${m}`
454
+ });
372
455
  });
373
456
  const modelSchema = z.object({
374
457
  components: declaredComponentsSchema.optional(),
375
458
  types: z.array(contentType).refine((ts) => new Set(ts.map((t) => t.id)).size === ts.length, { message: "型の id が重複" }).superRefine((ts, ctx) => {
376
459
  const ids = new Set(ts.map((t) => t.id));
377
- for (const t of ts) for (const to of referenceTargets([...t.core ?? [], ...t.fields])) if (!ids.has(to)) ctx.addIssue({
460
+ for (const t of ts) for (const to of referenceTargets(t.fields)) if (!ids.has(to)) ctx.addIssue({
378
461
  code: "custom",
379
462
  message: `型 ${t.id} の参照先 ${to} がスキーマにない`
380
463
  });
381
464
  for (const t of ts) {
382
- const keys = new Set([...t.core ?? [], ...t.fields].map((f) => f.key));
465
+ const keys = new Set(t.fields.map((f) => f.key));
383
466
  for (const c of t.counts ?? []) {
384
467
  const from = ts.find((x) => x.id === c.from);
385
- const ref = from && [...from.core ?? [], ...from.fields].find((f) => f.key === c.field);
468
+ const ref = from && from.fields.find((f) => f.key === c.field);
386
469
  if (ref?.type !== "reference" || ref.to !== t.id) ctx.addIssue({
387
470
  code: "custom",
388
471
  message: `型 ${t.id} の被参照の件数 ${c.key}: 型 ${c.from} の参照の項目 ${c.field} がこの型を指していない`
@@ -396,13 +479,13 @@ const modelSchema = z.object({
396
479
  })
397
480
  }).superRefine((m, ctx) => {
398
481
  const known = /* @__PURE__ */ new Set([...CORE_COMPONENTS.map((c) => c.name), ...(m.components ?? []).map((c) => c.name)]);
399
- for (const t of m.types) for (const { key, name } of bodyComponents([...t.core ?? [], ...t.fields])) if (!known.has(name)) ctx.addIssue({
482
+ for (const t of m.types) for (const { key, name } of bodyComponents(t.fields)) if (!known.has(name)) ctx.addIssue({
400
483
  code: "custom",
401
484
  message: `型 ${t.id} の本文の項目 ${key} の部品 ${name} がコアの部品にも宣言 components にもない`
402
485
  });
403
486
  });
404
487
  /** コンテンツスキーマの JSON を検査して返却(形が合わなければ例外、理由は zod の issues) */
405
- const parseModel = (json) => modelSchema.parse(json);
488
+ const parseModel = (json) => modelSchema.parse(normalizeLegacyModel(json));
406
489
  // ../model/src/screens.ts
407
490
  /** メニューにある全項目の id(グループの中も、上から順) */
408
491
  const menuItemsOf = (menu) => menu.flatMap((e) => typeof e === "string" ? [e] : e.items);
@@ -443,9 +526,8 @@ const DEFAULT_SCHEMA = { "types": [
443
526
  {
444
527
  "id": "post",
445
528
  "label": "投稿",
446
- "table": "articles",
447
529
  "publishing": "review",
448
- "core": [
530
+ "fields": [
449
531
  {
450
532
  "key": "title",
451
533
  "label": "タイトル",
@@ -498,24 +580,47 @@ const DEFAULT_SCHEMA = { "types": [
498
580
  "components": ["gallery", "link-card"]
499
581
  }
500
582
  },
501
- {
502
- "key": "published_at",
503
- "label": "公開日時",
504
- "type": "datetime"
505
- },
506
583
  {
507
584
  "key": "revised_at",
508
585
  "label": "更新日時",
509
586
  "type": "datetime"
587
+ },
588
+ {
589
+ "key": "tags",
590
+ "label": "タグ",
591
+ "type": "reference",
592
+ "to": "tag",
593
+ "multiple": true
594
+ },
595
+ {
596
+ "key": "excerpt",
597
+ "label": "抜粋",
598
+ "type": "textarea",
599
+ "computed": {
600
+ "kind": "excerpt",
601
+ "from": "body"
602
+ }
603
+ },
604
+ {
605
+ "key": "read_min",
606
+ "label": "読了の分",
607
+ "type": "number",
608
+ "computed": {
609
+ "kind": "read_min",
610
+ "from": "body"
611
+ }
612
+ },
613
+ {
614
+ "key": "links",
615
+ "label": "リンク先",
616
+ "type": "text",
617
+ "multiple": true,
618
+ "computed": {
619
+ "kind": "links",
620
+ "from": "body"
621
+ }
510
622
  }
511
623
  ],
512
- "fields": [{
513
- "key": "tags",
514
- "label": "タグ",
515
- "type": "reference",
516
- "to": "tag",
517
- "multiple": true
518
- }],
519
624
  "pages": [{
520
625
  "path": "/posts/{id}",
521
626
  "public": [{
@@ -565,7 +670,7 @@ const DEFAULT_SCHEMA = { "types": [
565
670
  "id": "image",
566
671
  "label": "画像",
567
672
  "description": "見せたい点(hotspot)と切り抜き(crop)を画像ごとに1つ保持、切り抜く場所すべてで共通",
568
- "core": [
673
+ "fields": [
569
674
  {
570
675
  "key": "mime_type",
571
676
  "label": "形式",
@@ -600,13 +705,12 @@ const DEFAULT_SCHEMA = { "types": [
600
705
  "type": "text",
601
706
  "description": "使用する側の既定の値、使用する場所ごとに上書き可"
602
707
  }
603
- ],
604
- "fields": []
708
+ ]
605
709
  },
606
710
  {
607
711
  "id": "video",
608
712
  "label": "動画",
609
- "core": [
713
+ "fields": [
610
714
  {
611
715
  "key": "mime_type",
612
716
  "label": "形式",
@@ -654,8 +758,7 @@ const DEFAULT_SCHEMA = { "types": [
654
758
  "accept": ["image"],
655
759
  "description": "読み込み前・動きを抑える設定・OG で代わりに表示"
656
760
  }
657
- ],
658
- "fields": []
761
+ ]
659
762
  }
660
763
  ] };
661
764
  /** 既定の画面設定(新しい媒体の作成で適用する中立の値、移入する screens の土台) */
@@ -107,10 +107,10 @@ function typeOf(model, id) {
107
107
  if (!type) throw new Error(`コンテンツスキーマに型 ${id} がない`);
108
108
  return type;
109
109
  }
110
- /** 選択肢の項目の選択肢(コアの項目と追加の項目から、まとまりの中の項目は `links.kind` の形、選択肢の項目でなければ空) */
110
+ /** 選択肢の項目の選択肢(まとまりの中の項目は `links.kind` の形、選択肢の項目でなければ空) */
111
111
  function optionsOf(type, key) {
112
112
  const [head, ...rest] = key.split(".");
113
- let field = [...type.core ?? [], ...type.fields].find((f) => f.key === head);
113
+ let field = type.fields.find((f) => f.key === head);
114
114
  for (const k of rest) field = field?.type === "group" ? field.fields.find((f) => f.key === k) : void 0;
115
115
  return field?.type === "select" ? field.options : [];
116
116
  }
@@ -45,8 +45,6 @@ export declare function createDeliveryClient(delivery: Delivery, { drafts, at }?
45
45
  description?: string;
46
46
  format?: import("../../model/src/index.js").ContentFormat;
47
47
  publishing?: import("../../model/src/index.js").Publishing;
48
- table?: string;
49
- core?: readonly import("../../model/src/index.js").FieldDef[];
50
48
  fields: readonly import("../../model/src/index.js").FieldDef[];
51
49
  pages?: readonly import("../../model/src/index.js").PageDef[];
52
50
  counts?: readonly import("../../model/src/index.js").CountDef[];
@@ -3,4 +3,4 @@ export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media
3
3
  export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../model/src/index.js';
4
4
  export { listParams, type ListFilter, type ListQuery, type Page } from './query.js';
5
5
  export { createDeliveryClock, fieldOf, limitOf, optionLabels, pagePath, type DeliveryClock } from './schema.js';
6
- export type { DeliveredAsset, FieldValue, Item, ItemOf } from './values.js';
6
+ export type { DeliveredAsset, FieldValue, Item, ItemMeta, ItemOf } from './values.js';
@@ -29,13 +29,14 @@ type SortableKey<T extends ContentType> = FieldsOf<T> extends infer F ? (F exten
29
29
  type: 'text' | 'number' | 'date' | 'datetime' | 'select' | 'checkbox';
30
30
  key: infer K extends string;
31
31
  } ? K : never) : never;
32
- /** 並べ替えの key(項目・公開日時・更新日時・id) */
33
- type OrderKey<T extends ContentType> = SortableKey<T> | 'published_at' | 'updated_at' | 'id';
34
- /** 絞り込みの指定(項目の key ごと、公開日時・更新日時は範囲) */
32
+ /** 並べ替えの key(項目・公開日時・作成日時・更新日時・id) */
33
+ type OrderKey<T extends ContentType> = SortableKey<T> | 'published_at' | 'created_at' | 'updated_at' | 'id';
34
+ /** 絞り込みの指定(項目の key ごと、公開日時・作成日時・更新日時は範囲) */
35
35
  export type ListFilter<T extends ContentType = ContentType> = Readonly<{
36
36
  [F in FieldsOf<T> as [FilterValue<F>] extends [never] ? never : F['key']]?: FilterValue<F>;
37
37
  } & {
38
38
  published_at?: Range<string>;
39
+ created_at?: Range<string>;
39
40
  updated_at?: Range<string>;
40
41
  }>;
41
42
  /**
@@ -1,7 +1,7 @@
1
1
  import { type Clock, type ContentModel, type DateString, type FieldDef, type PageEntry } from '../../model/src/index.js';
2
2
  /** 選択肢の項目から表示名の表(値 → 表示名、key はまとまりの中なら `links.kind` の形、選択肢の項目でなければ空) */
3
3
  export declare const optionLabels: <K extends string = string>(schema: ContentModel, type: string, key: string) => Readonly<Record<K, string>>;
4
- /** 型の項目(コアの項目と追加の項目から、なければ例外: 導入先の設定の誤り) */
4
+ /** 型の項目(なければ例外: 導入先の設定の誤り) */
5
5
  export declare function fieldOf(schema: ContentModel, type: string, key: string): FieldDef;
6
6
  /** 項目の宣言の上限(宣言がなければ例外) */
7
7
  export declare function limitOf(field: FieldDef, name: 'maxItems' | 'maxLength'): number;
@@ -1,6 +1,6 @@
1
1
  import type { Body } from '../../body/src/index.js';
2
2
  import type { ContentType, Crop, DateString, FieldDef, Hotspot } from '../../model/src/index.js';
3
- /** 配信 API の1件の値(項目の key と id・updated_at) */
3
+ /** 配信 API の1件の値(id・項目の key・メタ情報 _meta) */
4
4
  export type Item = Readonly<Record<string, unknown>>;
5
5
  /** 配信 API が解決した実体(url は配信 URL で配信のオリジンがある CMS だけ、hotspot・crop は元の画像に対する割合) */
6
6
  export type DeliveredAsset = Readonly<{
@@ -52,12 +52,18 @@ type GroupValue<G extends FieldDef> = {
52
52
  export type FieldValue<F extends FieldDef> = (F extends {
53
53
  multiple: true;
54
54
  } ? readonly SingleValue<F>[] : SingleValue<F>) | null;
55
- /** 型の項目(コアの項目と追加の項目) */
56
- export type FieldsOf<T extends ContentType> = (T['core'] extends readonly (infer C extends FieldDef)[] ? C : never) | T['fields'][number];
57
- /** 型の宣言から導く1件の値(id・更新日時と、項目の key ごとの値) */
55
+ /** 型の項目 */
56
+ export type FieldsOf<T extends ContentType> = T['fields'][number];
57
+ /** 1件のメタ情報(作成・更新の日時、公開日時は公開の管理がある型だけで未公開は null、日時は `ISO 8601` の UTC) */
58
+ export type ItemMeta = Readonly<{
59
+ created_at: string;
60
+ updated_at: string;
61
+ published_at?: string | null;
62
+ }>;
63
+ /** 型の宣言から導く1件の値(id・項目の key ごとの値・メタ情報 _meta) */
58
64
  export type ItemOf<T extends ContentType> = Readonly<{
59
65
  id: string;
60
- updated_at: string;
66
+ _meta: ItemMeta;
61
67
  } & {
62
68
  [F in FieldsOf<T> as F['key']]: FieldValue<F>;
63
69
  }>;
@@ -1,20 +1,23 @@
1
- import type { ContentModel, ContentType } from './model.js';
2
- /**
3
- * コアの表の性質
4
- * - editorial: 編集の機能の列 status・published_at・version・review_note・assignee
5
- * - derived: 本文から保存時に作成する列 excerpt・read_min・links
6
- * - sources: 共有の行(出典)を付属の表 article_sources で結び付ける
7
- * - shared: 共有の行を持つ表(複数の行から結び付ける)
8
- */
9
- export type TableTraits = Readonly<{
10
- editorial: boolean;
11
- derived: boolean;
12
- sources: boolean;
13
- shared: boolean;
1
+ import { type ContentModel, type ContentType, type FieldDef } from './model.js';
2
+ /** 出典の項目と、内側の項目の key */
3
+ export declare const SOURCES_FIELD: {
4
+ readonly key: "sources";
5
+ readonly source: "source";
6
+ readonly note: "note";
7
+ };
8
+ /** 出典の項目の定義(参照の項目 source の参照先 to) */
9
+ export type SourcesField = Extract<FieldDef, {
10
+ type: 'group';
11
+ }> & Readonly<{
12
+ to: string;
14
13
  }>;
15
- /** 表の性質(コアの表にない表はすべてなし) */
16
- export declare const tableTraitsOf: (table: string | null | undefined) => TableTraits;
17
- /** 表の性質を持つ型(スキーマの順で先頭、なければ null: コアの表は1つの型だけが保存) */
18
- export declare const typeWithTrait: (model: ContentModel, trait: keyof TableTraits) => ContentType | null;
19
- /** 表の性質を持つ型(なければ例外: その型を前提にする画面・処理だけが使用) */
20
- export declare function requiredTypeWithTrait(model: ContentModel, trait: keyof TableTraits): ContentType;
14
+ /** 主の型(スキーマの順で先頭、なければ null) */
15
+ export declare const primaryTypeOf: (model: ContentModel) => ContentType | null;
16
+ /** 主の型(なければ例外: その型を前提にする画面・処理だけが使用) */
17
+ export declare function requiredPrimaryTypeOf(model: ContentModel): ContentType;
18
+ /** 型の出典の項目(なければ null) */
19
+ export declare function sourcesFieldOf(type: ContentType): SourcesField | null;
20
+ /** 出典の型(出典の項目の参照先でスキーマの順で先頭、なければ null) */
21
+ export declare function sourceTypeOf(model: ContentModel): ContentType | null;
22
+ /** 出典の値にある参照の ID(注の順、空と形の誤りは除外) */
23
+ export declare const sourceIdsOf: (value: unknown) => string[];
@@ -1,4 +1,4 @@
1
- import type { ContentModel } from './model.js';
1
+ import { type ContentModel } from './model.js';
2
2
  /** 変更の扱い */
3
3
  export type ChangeLevel = 'safe' | 'confirm' | 'forbidden';
4
4
  /** 変更の種類 */
@@ -13,11 +13,9 @@ export type SchemaChange = Readonly<{
13
13
  value?: string;
14
14
  message: string;
15
15
  }>;
16
- /** 差分の条件(fixed はコードで固定の定義、history は保存済みの版より古い過去の版) */
16
+ /** 差分の条件(history は保存済みの版より古い過去の版) */
17
17
  export type DiffOptions = Readonly<{
18
- fixed?: ContentModel;
19
18
  history?: readonly ContentModel[];
20
19
  }>;
21
20
  /** 保存済みの版 prev から新しい定義 next への変更(型の順、項目の順) */
22
21
  export declare function diffSchema(prev: ContentModel, next: ContentModel, options?: DiffOptions): SchemaChange[];
23
- export declare function withFixed(fixed: ContentModel, stored: ContentModel): ContentModel;
@@ -3,12 +3,13 @@ export { assetRefSchema, cropSchema, hotspotSchema, mediaSchema, type Asset, typ
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
5
  export { IMAGE_SIZE_MAX, imageParamsOf, mediaExtOf, mediaMimeOf, mediaPathOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from './mediaUrl.js';
6
- export { diffSchema, withFixed, type ChangeKind, type ChangeLevel, type DiffOptions, type SchemaChange } from './changes.js';
7
- export { requiredTypeWithTrait, tableTraitsOf, typeWithTrait, type TableTraits } from './capabilities.js';
6
+ export { diffSchema, type ChangeKind, type ChangeLevel, type DiffOptions, type SchemaChange } from './changes.js';
7
+ export { 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 { idInputOf, imageFieldOf, initialValueOf, isEditorial, isEntryType, isListType, isSingleton, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, tableOf, titleFieldOf, toneOf, typeOf, 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 } from './model.js';
11
- export { hasPublicPage, imageFrameOf, pagePathOf, publicPathOf, type ImageFrame, type ImageFrameDef, type PageDef, type PageEntry, type PageRule } from './page.js';
10
+ export { entryRefsOf, type EntryRef } from './refs.js';
11
+ export { bodyFieldOf, bodyFieldsOf, computedFieldsOf, idInputOf, imageFieldOf, initialValueOf, isEditorial, isInputField, isListType, isMediaType, isSingleton, isStoredType, MEDIA_TYPE_IDS, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, titleFieldOf, toneOf, typeOf, type ComputedKind, 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 } from './model.js';
12
+ export { hasPublicPage, imageFrameOf, pagePathOf, publicPathOf, withPagesFrom, type ImageFrame, type ImageFrameDef, type PageDef, type PageEntry, type PageRule } from './page.js';
12
13
  export { schemaRulesOf, type SchemaRule } from './rules.js';
13
14
  export { parseModel } from './schema.js';
14
15
  export { menuItemsOf, parseScreens, placedItemsOf, resolveLayout, resolveMenu, typeItem, typeOfItem, viewItem, viewOfItem, type EditorLayout, type EditorSection, type MenuEntry, type MenuGroup, type ResolvedMenuEntry, type ScreenSettings } from './screens.js';
@@ -0,0 +1,2 @@
1
+ /** 古い形のスキーマの JSON を今の形に(形の分からない値はそのまま、検査は parseModel) */
2
+ export declare function normalizeLegacyModel(json: unknown): unknown;
@@ -15,6 +15,15 @@ type FieldBase = Readonly<{
15
15
  adminOnly?: boolean;
16
16
  /** 未定の null にできる期限: 型の最上位の項目 key に値が入ると未定なし(方針のページの制定日など) */
17
17
  pendingUntil?: string;
18
+ /** 計算の項目(保存のときに本文の項目から作成、入力の対象外) */
19
+ computed?: FieldComputed;
20
+ }>;
21
+ /** 計算の項目の種類(excerpt は導入の段落・read_min は読了の分・links はサイト内へのリンク先) */
22
+ export type ComputedKind = 'excerpt' | 'read_min' | 'links';
23
+ /** 計算の項目の宣言(from は同じ型の最上位にある本文の項目の key) */
24
+ export type FieldComputed = Readonly<{
25
+ kind: ComputedKind;
26
+ from: string;
18
27
  }>;
19
28
  /** 項目の対象の条件(同じ階層にある選択肢の項目 key の値が in のいずれか) */
20
29
  export type FieldCondition = Readonly<{
@@ -67,7 +76,7 @@ export type ReferenceRules = Readonly<{
67
76
  published?: boolean;
68
77
  linkedBy?: string;
69
78
  }>;
70
- /** メディアの種類(コアの型 image・video の id と同じ) */
79
+ /** メディアの種類(メディアの型 image・video の id と同じ) */
71
80
  export type MediaKind = 'image' | 'video';
72
81
  /** 項目の定義 */
73
82
  export type FieldDef = FieldBase & Readonly<{
@@ -95,7 +104,7 @@ export type FieldDef = FieldBase & Readonly<{
95
104
  } & Counts) | ({
96
105
  type: 'reference';
97
106
  to: string;
98
- multiple?: boolean; /** 候補の絞り込み(参照先のコアの項目の key と値、例: 企画の種類が特集) */
107
+ multiple?: boolean; /** 候補の絞り込み(参照先の項目の key と値、例: 企画の種類が特集) */
99
108
  where?: Readonly<Record<string, string>>; /** 候補にない名前を追加の依頼で付けられる(参照先は一覧で管理する汎用の型、承認で行を作成) */
100
109
  requests?: boolean; /** 値が空のときの表示名(持ち主のいない行の名義など) */
101
110
  emptyLabel?: string;
@@ -117,19 +126,14 @@ export type ContentFormat = 'list' | 'object';
117
126
  /** 公開の管理(なし・下書きと公開・レビューあり) */
118
127
  export type Publishing = 'none' | 'draft' | 'review';
119
128
  /**
120
- * 型の定義
121
- * - id: コアの型の ID か、追加の型の ID
122
- * - core: コアで固定の項目、fields: 導入先の追加の項目
123
- * - format: 形式(なしはリスト形式)、オブジェクト形式はコアの項目なし
129
+ * コンテンツタイプ
130
+ * - id: 型の ID(メディアの型は image・video)
131
+ * - format: 形式(なしはリスト形式)
124
132
  * - publishing: 公開の管理(なしは管理なし)
125
- * - table: コアの項目を持つ型の表
126
133
  * - pages: サイトのページの URL と公開中の条件(なしはページのない型、page.ts)
127
- */
128
- /**
129
- * コンテンツタイプ
130
134
  * - counts: 被参照の件数(ページの公開中の条件・ページの公開で必須の判定に使用)
131
135
  * - og: サイトの OG 画像のパスの雛形({key} は行の値、管理画面の OG 画像のプレビュー)
132
- * - idInput: 作成のときに管理画面で ID を入力できる(汎用の型だけ、ページのある型は常に入力可、空欄は自動)
136
+ * - idInput: 作成のときに管理画面で ID を入力できる(ページのある型は常に入力可、空欄は自動)
133
137
  */
134
138
  export type ContentType = Readonly<{
135
139
  id: string;
@@ -137,8 +141,6 @@ export type ContentType = Readonly<{
137
141
  description?: string;
138
142
  format?: ContentFormat;
139
143
  publishing?: Publishing;
140
- table?: string;
141
- core?: readonly FieldDef[];
142
144
  fields: readonly FieldDef[];
143
145
  pages?: readonly PageDef[];
144
146
  counts?: readonly CountDef[];
@@ -155,15 +157,17 @@ export type FieldProblem = Readonly<{
155
157
  path: string;
156
158
  message: string;
157
159
  }>;
158
- /** 汎用の表 entries に保存する型か(コアの項目がない型、コアの項目がある型は専用の表) */
159
- export declare const isEntryType: (type: ContentType) => boolean;
160
- /** 作成のときに ID を入力できる型か(汎用の型で、宣言 idInput かページを持つ型) */
160
+ /** メディアの型の id(項目の種類 media の accept と同じ、行は表 assets) */
161
+ export declare const MEDIA_TYPE_IDS: readonly MediaKind[];
162
+ /** メディアの型か(画像・動画、行は表 assets で一覧・作成・配信の対象外) */
163
+ export declare const isMediaType: (type: ContentType) => boolean;
164
+ /** 作成のときに ID を入力できる型か(メディアの型を除き、宣言 idInput かページを持つ型) */
161
165
  export declare const idInputOf: (type: ContentType) => boolean;
162
166
  /** オブジェクト形式の型か(行は表 entries の id が型の id の1行、一覧・作成の対象外) */
163
167
  export declare const isSingleton: (type: ContentType) => boolean;
164
168
  /** 公開の管理(なしは none) */
165
169
  export declare const publishingOf: (type: ContentType) => Publishing;
166
- /** 一覧で行を追加・管理する汎用の型か(取材相手・トピックなど、オブジェクト形式の型を除く) */
170
+ /** 一覧で行を追加・管理する型か(オブジェクト形式の型・メディアの型を除く) */
167
171
  export declare const isListType: (type: ContentType) => boolean;
168
172
  /**
169
173
  * 編集の機能を持つ型か(公開の管理がある一覧の型)
@@ -171,11 +175,11 @@ export declare const isListType: (type: ContentType) => boolean;
171
175
  * - 提出と差し戻し: レビューありの型だけ
172
176
  */
173
177
  export declare const isEditorial: (type: ContentType) => boolean;
174
- /** 型の行を保存する表(汎用の型は entries、表を持たない型は null) */
175
- export declare const tableOf: (type: ContentType) => string | null;
178
+ /** 型の行を表 entries に保存するか(メディアの型を除くすべて) */
179
+ export declare const isStoredType: (type: ContentType) => boolean;
176
180
  /** コンテンツスキーマの型(id から、なければ例外: 導入先の設定の誤り) */
177
181
  export declare function typeOf(model: ContentModel, id: string): ContentType;
178
- /** 選択肢の項目の選択肢(コアの項目と追加の項目から、まとまりの中の項目は `links.kind` の形、選択肢の項目でなければ空) */
182
+ /** 選択肢の項目の選択肢(まとまりの中の項目は `links.kind` の形、選択肢の項目でなければ空) */
179
183
  export declare function optionsOf(type: ContentType, key: string): readonly SelectOption[];
180
184
  /** 新しい行の初期値(セレクトは項目の initial、なければ先頭の選択肢、セレクトでなければ空文字) */
181
185
  export declare const initialValueOf: (type: ContentType, key: string) => string;
@@ -183,11 +187,23 @@ export declare const initialValueOf: (type: ContentType, key: string) => string;
183
187
  export declare const toneOf: (type: ContentType, key: string, value: string) => OptionTone;
184
188
  /** 選択肢の表示名(なければ値のまま) */
185
189
  export declare const optionLabelOf: (type: ContentType, key: string, value: string) => string;
190
+ /** 入力する項目か(計算の項目を除く) */
191
+ export declare const isInputField: (f: FieldDef) => boolean;
186
192
  /** 型の表示名の項目(複数でない文字の項目で先頭、なければ null: 行の表示名に使用) */
187
193
  export declare const titleFieldOf: (type: ContentType) => FieldDef | null;
188
- /** 型のサムネの項目(複数でない画像の項目で先頭、なければ null: 一覧・参照の候補に使用) */
194
+ /** 型のサムネの項目(複数でない画像の項目で先頭、なければ null: 一覧・参照の候補・アイキャッチに使用) */
189
195
  export declare const imageFieldOf: (type: ContentType) => FieldDef | null;
190
- /** 追加の依頼ができる参照の項目(コアと追加の項目、参照先が一覧で管理する汎用の型で requests の指定があるもの、タグ・分類など) */
196
+ /** 型の本文の項目(最上位で先頭、なければ null: 本文の Markdown・点検・制作の案内に使用) */
197
+ export declare const bodyFieldOf: (type: ContentType) => Extract<FieldDef, {
198
+ type: "body";
199
+ }> | null;
200
+ /** 最上位で本文の種類の項目(表 entry_bodies に保存、一覧では取得しない) */
201
+ export declare const bodyFieldsOf: (type: ContentType) => Extract<FieldDef, {
202
+ type: "body";
203
+ }>[];
204
+ /** 型の計算の項目(最上位) */
205
+ export declare const computedFieldsOf: (type: ContentType) => FieldDef[];
206
+ /** 追加の依頼ができる参照の項目(参照先が一覧で管理する型で requests の指定があるもの、タグ・分類など) */
191
207
  export declare function requestReferencesOf(model: ContentModel, type: ContentType): Extract<FieldDef, {
192
208
  type: 'reference';
193
209
  }>[];
@@ -1,4 +1,4 @@
1
- import type { ContentType, FieldCondition } from './model.js';
1
+ import type { ContentModel, ContentType, FieldCondition } from './model.js';
2
2
  /** ページの行の値(項目の key と値、表の列・集計の値も含む) */
3
3
  export type PageEntry = Readonly<Record<string, unknown>>;
4
4
  /**
@@ -43,5 +43,10 @@ export declare function pagePathOf(type: ContentType, entry: PageEntry): string
43
43
  export declare function hasPublicPage(type: ContentType, entry: PageEntry, now: string): boolean;
44
44
  /** 公開中のページのパス(公開前・予約中・ページのない行は null、now は今の時刻の ISO 文字列) */
45
45
  export declare function publicPathOf(type: ContentType, entry: PageEntry, now: string): string | null;
46
- /** 画像の項目 key の枠(コアの項目と追加の項目の最上位から、枠の宣言がなければ null) */
46
+ /** 画像の項目 key の枠(最上位の項目から、枠の宣言がなければ null) */
47
47
  export declare function imageFrameOf(type: ContentType, key: string, entry: PageEntry): ImageFrame | null;
48
+ /**
49
+ * 保存済みでページの宣言がない型に、最初の版の宣言を補う(宣言の導入より前に保存した版の読み込み)
50
+ * - ページなしにするのは空の配列(補わない)
51
+ */
52
+ export declare function withPagesFrom(first: ContentModel, stored: ContentModel): ContentModel;
@@ -0,0 +1,10 @@
1
+ import type { FieldDef } from './model.js';
2
+ /** 参照の逆引きの1行(toType は参照先の型、toId は参照先の ID) */
3
+ export type EntryRef = Readonly<{
4
+ key: string;
5
+ position: number;
6
+ toType: string;
7
+ toId: string;
8
+ }>;
9
+ /** 行の値の参照(fields は型の項目、values は行の値) */
10
+ export declare function entryRefsOf(fields: readonly FieldDef[], values: unknown): EntryRef[];
@@ -1,2 +1,2 @@
1
1
  export { createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from '../../delivery-client/src/index.js';
2
- export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from '../../delivery-client/src/index.js';
2
+ export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemMeta, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from '../../delivery-client/src/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",