@pramen/cms 0.0.61 → 0.0.64

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { HandlerContext, Policy, FileRef, BootstrapFn, JsonValue, SchemaDef } from "@pramen/server";
1
+ import type { HandlerContext, Policy, FileRef, BootstrapFn, DataMigration, JsonValue, SchemaDef } from "@pramen/server";
2
2
  import type { EnvBag } from "@pramen/server";
3
3
  /** A field in a block type's (or content type's) field schema. Recursive: a `repeater`
4
4
  * or `group` nests `fields`. Mirrors WollyCMS's FieldDefinition. */
@@ -887,6 +887,9 @@ export declare const cmsSchema: {
887
887
  } & {
888
888
  readonly default: false;
889
889
  };
890
+ appliesTo: {
891
+ readonly type: "json";
892
+ };
890
893
  createdAt: {
891
894
  readonly type: "text";
892
895
  } & {
@@ -953,6 +956,11 @@ export declare const cmsSchema: {
953
956
  readonly target: "cms_page_terms";
954
957
  readonly column: string;
955
958
  };
959
+ media: {
960
+ readonly kind: "hasMany";
961
+ readonly target: "cms_media_terms";
962
+ readonly column: string;
963
+ };
956
964
  }>;
957
965
  cms_page_terms: import("@pramen/server").EntityDef<{
958
966
  id: {
@@ -991,6 +999,43 @@ export declare const cmsSchema: {
991
999
  readonly onDelete: import("@pramen/server").OnDelete | undefined;
992
1000
  };
993
1001
  }>;
1002
+ cms_media_terms: import("@pramen/server").EntityDef<{
1003
+ id: {
1004
+ readonly type: "uuid";
1005
+ } & {
1006
+ readonly generated: true;
1007
+ } & {
1008
+ readonly primaryKey: true;
1009
+ readonly notNull: true;
1010
+ };
1011
+ mediaId: {
1012
+ readonly type: "uuid";
1013
+ } & {
1014
+ readonly notNull: true;
1015
+ } & {
1016
+ readonly index: true;
1017
+ };
1018
+ termId: {
1019
+ readonly type: "uuid";
1020
+ } & {
1021
+ readonly notNull: true;
1022
+ } & {
1023
+ readonly index: true;
1024
+ };
1025
+ }, {
1026
+ media: {
1027
+ readonly kind: "belongsTo";
1028
+ readonly target: "cms_media";
1029
+ readonly column: string;
1030
+ readonly onDelete: import("@pramen/server").OnDelete | undefined;
1031
+ };
1032
+ term: {
1033
+ readonly kind: "belongsTo";
1034
+ readonly target: "cms_terms";
1035
+ readonly column: string;
1036
+ readonly onDelete: import("@pramen/server").OnDelete | undefined;
1037
+ };
1038
+ }>;
994
1039
  cms_widget_areas: import("@pramen/server").EntityDef<{
995
1040
  id: {
996
1041
  readonly type: "uuid";
@@ -1046,6 +1091,19 @@ export declare const cmsSchema: {
1046
1091
  file: {
1047
1092
  readonly type: "fileRef";
1048
1093
  };
1094
+ filename: {
1095
+ readonly type: "text";
1096
+ } & {
1097
+ readonly index: true;
1098
+ };
1099
+ contentType: {
1100
+ readonly type: "text";
1101
+ } & {
1102
+ readonly index: true;
1103
+ };
1104
+ size: {
1105
+ readonly type: "integer";
1106
+ };
1049
1107
  alt: {
1050
1108
  readonly type: "text";
1051
1109
  };
@@ -1059,12 +1117,44 @@ export declare const cmsSchema: {
1059
1117
  } & {
1060
1118
  readonly defaultExpr: string;
1061
1119
  };
1062
- }, Record<string, never>>;
1120
+ }, {
1121
+ terms: {
1122
+ readonly kind: "manyToMany";
1123
+ readonly target: "cms_terms";
1124
+ readonly through: string;
1125
+ readonly sourceColumn: string;
1126
+ readonly targetColumn: string;
1127
+ };
1128
+ }>;
1063
1129
  };
1130
+ /** How a media list may be ordered. */
1131
+ export type MediaSort = "newest" | "oldest" | "name" | "name_desc" | "largest" | "smallest";
1132
+ /** The coarse type buckets the library filters by — the first segment of a MIME type, which
1133
+ * is the distinction someone browsing a library actually makes ("show me the images"). */
1134
+ export declare const MEDIA_KINDS: readonly ["image", "video", "audio", "document", "other"];
1135
+ export type MediaKind = (typeof MEDIA_KINDS)[number];
1136
+ /** The object types a vocabulary can be applied to. A CLOSED vocabulary, like `MEDIA_KINDS`:
1137
+ * it decides which panels offer a taxonomy AND which assignments are accepted, so an unknown
1138
+ * value must not be storable. */
1139
+ export declare const TAXONOMY_TARGETS: readonly ["page", "media"];
1140
+ export type TaxonomyTarget = (typeof TAXONOMY_TARGETS)[number];
1141
+ /** Whether a vocabulary applies to `target`.
1142
+ *
1143
+ * The permissive readings all collapse to TRUE, deliberately: NULL (never narrowed, or written
1144
+ * before the column existed), a non-array, an array of things that are not targets. A stored
1145
+ * value nobody can interpret must not silently stop a vocabulary from working — the failure
1146
+ * would be a taxonomy that has quietly vanished from every panel, with the row still there. */
1147
+ export declare function taxonomyApplies(row: {
1148
+ appliesTo?: unknown;
1149
+ }, target: TaxonomyTarget): boolean;
1064
1150
  /** Block Kit — custom admin pages, described as JSON and rendered by the editor. See
1065
1151
  * `./blockkit`. Re-exported so a host imports `adminPage` beside `collection`. */
1066
- export { adminPage, createAdminPageHandlers, normalizeAdminResponse, validateAdminPages, MAX_ADMIN_BLOCK_DEPTH, } from "./blockkit";
1067
- export type { AdminBlock, AdminButton, AdminElement, AdminInput, AdminInteractionType, AdminPageDef, AdminPageHandlerOpts, AdminPageInteraction, AdminPageMeta, AdminPageResponse, AdminText, } from "./blockkit";
1152
+ export { adminPage, createAdminPageHandlers, normalizeAdminResponse, validateAdminPages, ADMIN_ELEMENT_TYPES, ADMIN_PAGE_KINDS, MAX_ADMIN_BLOCK_DEPTH, } from "./blockkit";
1153
+ export type { AdminBlock, AdminButton, AdminCell, AdminElement, AdminInput, AdminInteractionType, AdminPageDef, AdminPageHandlerOpts, AdminPageInteraction, AdminPageKind, AdminPageMeta, AdminPageResponse, AdminScreenDef, AdminText, } from "./blockkit";
1154
+ /** Custom admin PANELS — a project's own React screen inside the editor's chrome, for the
1155
+ * screens a server-driven vocabulary cannot carry. See `./panel`. */
1156
+ export { adminPanel, isAdminPanel } from "./panel";
1157
+ export type { AdminPanelDef } from "./panel";
1068
1158
  /**
1069
1159
  * Columns this package wrote in the pre-ISO space form that the SCHEMA cannot identify.
1070
1160
  *
@@ -1478,28 +1568,48 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
1478
1568
  }, {
1479
1569
  ok: true;
1480
1570
  }>;
1481
- /** Every vocabulary. PUBLIC like content types, a taxonomy's slug is structural (it
1482
- * is a URL segment) and a front end routes on it. */
1483
- listTaxonomies: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
1571
+ /** Every vocabulary, or just the ones that classify `target`. PUBLIC like content
1572
+ * types, a taxonomy's slug is structural (it is a URL segment) and a front end routes on
1573
+ * it.
1574
+ *
1575
+ * Narrowed HERE rather than in each caller, so the page panel, the media panel and the
1576
+ * write-side guard cannot disagree about what a vocabulary applies to. In memory, because
1577
+ * `appliesTo` is a `t.json()` column that `where` cannot see into — which costs nothing:
1578
+ * this handler already reads every taxonomy, and nothing pages by them.
1579
+ *
1580
+ * No `target` means EVERY vocabulary, which is what the Taxonomies screen needs: the one
1581
+ * place that edits `appliesTo` must be able to see a vocabulary it has narrowed away. */
1582
+ listTaxonomies: import("@pramen/server").Handler<{
1583
+ target?: TaxonomyTarget;
1584
+ }, Record<string, unknown>[]>;
1484
1585
  createTaxonomy: import("@pramen/server").Handler<{
1485
1586
  slug: string;
1486
1587
  label: string;
1487
1588
  pluralLabel?: string;
1488
1589
  description?: string;
1489
1590
  hierarchical?: boolean;
1591
+ appliesTo?: TaxonomyTarget[] | null;
1490
1592
  }, Record<string, unknown>>;
1491
1593
  /** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
1492
1594
  * is not mutable — the same rule content types and block types already follow.
1493
1595
  *
1494
1596
  * Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
1495
1597
  * would leave a stored hierarchy that no reader renders and no writer can clear, and
1496
- * flattening the terms silently is a destructive edit behind a checkbox. */
1598
+ * flattening the terms silently is a destructive edit behind a checkbox.
1599
+ *
1600
+ * NARROWING `appliesTo` is refused on exactly the same grounds, and it is the same bug:
1601
+ * dropping a target this vocabulary is already used for would strand those assignments —
1602
+ * still stored, still returned by `listPageTerms`/`listMediaTerms`, but invisible in the
1603
+ * panel that could remove them, because the panel only renders vocabularies that apply.
1604
+ * Unassign them first; then the narrowing is a settings change rather than a silent
1605
+ * orphaning. WIDENING is always fine — it strands nothing. */
1497
1606
  updateTaxonomy: import("@pramen/server").Handler<{
1498
1607
  id: string;
1499
1608
  label?: string;
1500
1609
  pluralLabel?: string | null;
1501
1610
  description?: string | null;
1502
1611
  hierarchical?: boolean;
1612
+ appliesTo?: TaxonomyTarget[] | null;
1503
1613
  }, Record<string, unknown> | undefined>;
1504
1614
  /** Delete a vocabulary. Its terms go with it, and their page assignments with those —
1505
1615
  * both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
@@ -1617,6 +1727,10 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
1617
1727
  listMedia: import("@pramen/server").Handler<{
1618
1728
  limit?: number;
1619
1729
  offset?: number;
1730
+ sort?: MediaSort;
1731
+ kind?: MediaKind;
1732
+ q?: string;
1733
+ term?: string;
1620
1734
  }, Record<string, unknown>[]>;
1621
1735
  getMedia: import("@pramen/server").Handler<{
1622
1736
  id: string;
@@ -1626,6 +1740,25 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
1626
1740
  id: string;
1627
1741
  alt: string | null;
1628
1742
  }, Record<string, unknown>>;
1743
+ /** A media asset's assigned terms.
1744
+ *
1745
+ * The media row is read FIRST, through `ctx.db`, so the caller's own scope decides
1746
+ * whether this answers — the junction and `cms_terms` are granted unscoped, so without
1747
+ * it anyone holding an id could read a TRASHED file's tags and the non-empty answer
1748
+ * would confirm the file exists. Same rule as `listPageTerms`, for the same reason. */
1749
+ listMediaTerms: import("@pramen/server").Handler<{
1750
+ mediaId: string;
1751
+ }, Term[]>;
1752
+ /** Replace a media asset's term assignments wholesale — set semantics, like
1753
+ * `setPageTerms`, and for the same reason: the panel holds the whole selection, and two
1754
+ * calls each patching one end of it race into a state neither asked for. */
1755
+ setMediaTerms: import("@pramen/server").Handler<{
1756
+ mediaId: string;
1757
+ termIds: string[];
1758
+ }, {
1759
+ ok: true;
1760
+ count: number;
1761
+ }>;
1629
1762
  /** Trash a media row. The R2 OBJECT IS KEPT — deleting the bytes here would make
1630
1763
  * `restoreMedia` a lie, and a block still referencing the id would render a dead url
1631
1764
  * with no way back. `purgeMedia` is what drops both — and `listTrash` is how you find
@@ -1787,6 +1920,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
1787
1920
  pagesByType: true;
1788
1921
  siteFurniture: true;
1789
1922
  codeDefinedTypes: true;
1923
+ mediaTerms: true;
1790
1924
  canEdit: boolean;
1791
1925
  }>;
1792
1926
  /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
@@ -2077,28 +2211,48 @@ export declare const cmsHandlers: {
2077
2211
  }, {
2078
2212
  ok: true;
2079
2213
  }>;
2080
- /** Every vocabulary. PUBLIC like content types, a taxonomy's slug is structural (it
2081
- * is a URL segment) and a front end routes on it. */
2082
- listTaxonomies: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
2214
+ /** Every vocabulary, or just the ones that classify `target`. PUBLIC like content
2215
+ * types, a taxonomy's slug is structural (it is a URL segment) and a front end routes on
2216
+ * it.
2217
+ *
2218
+ * Narrowed HERE rather than in each caller, so the page panel, the media panel and the
2219
+ * write-side guard cannot disagree about what a vocabulary applies to. In memory, because
2220
+ * `appliesTo` is a `t.json()` column that `where` cannot see into — which costs nothing:
2221
+ * this handler already reads every taxonomy, and nothing pages by them.
2222
+ *
2223
+ * No `target` means EVERY vocabulary, which is what the Taxonomies screen needs: the one
2224
+ * place that edits `appliesTo` must be able to see a vocabulary it has narrowed away. */
2225
+ listTaxonomies: import("@pramen/server").Handler<{
2226
+ target?: TaxonomyTarget;
2227
+ }, Record<string, unknown>[]>;
2083
2228
  createTaxonomy: import("@pramen/server").Handler<{
2084
2229
  slug: string;
2085
2230
  label: string;
2086
2231
  pluralLabel?: string;
2087
2232
  description?: string;
2088
2233
  hierarchical?: boolean;
2234
+ appliesTo?: TaxonomyTarget[] | null;
2089
2235
  }, Record<string, unknown>>;
2090
2236
  /** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
2091
2237
  * is not mutable — the same rule content types and block types already follow.
2092
2238
  *
2093
2239
  * Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
2094
2240
  * would leave a stored hierarchy that no reader renders and no writer can clear, and
2095
- * flattening the terms silently is a destructive edit behind a checkbox. */
2241
+ * flattening the terms silently is a destructive edit behind a checkbox.
2242
+ *
2243
+ * NARROWING `appliesTo` is refused on exactly the same grounds, and it is the same bug:
2244
+ * dropping a target this vocabulary is already used for would strand those assignments —
2245
+ * still stored, still returned by `listPageTerms`/`listMediaTerms`, but invisible in the
2246
+ * panel that could remove them, because the panel only renders vocabularies that apply.
2247
+ * Unassign them first; then the narrowing is a settings change rather than a silent
2248
+ * orphaning. WIDENING is always fine — it strands nothing. */
2096
2249
  updateTaxonomy: import("@pramen/server").Handler<{
2097
2250
  id: string;
2098
2251
  label?: string;
2099
2252
  pluralLabel?: string | null;
2100
2253
  description?: string | null;
2101
2254
  hierarchical?: boolean;
2255
+ appliesTo?: TaxonomyTarget[] | null;
2102
2256
  }, Record<string, unknown> | undefined>;
2103
2257
  /** Delete a vocabulary. Its terms go with it, and their page assignments with those —
2104
2258
  * both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
@@ -2216,6 +2370,10 @@ export declare const cmsHandlers: {
2216
2370
  listMedia: import("@pramen/server").Handler<{
2217
2371
  limit?: number;
2218
2372
  offset?: number;
2373
+ sort?: MediaSort;
2374
+ kind?: MediaKind;
2375
+ q?: string;
2376
+ term?: string;
2219
2377
  }, Record<string, unknown>[]>;
2220
2378
  getMedia: import("@pramen/server").Handler<{
2221
2379
  id: string;
@@ -2225,6 +2383,25 @@ export declare const cmsHandlers: {
2225
2383
  id: string;
2226
2384
  alt: string | null;
2227
2385
  }, Record<string, unknown>>;
2386
+ /** A media asset's assigned terms.
2387
+ *
2388
+ * The media row is read FIRST, through `ctx.db`, so the caller's own scope decides
2389
+ * whether this answers — the junction and `cms_terms` are granted unscoped, so without
2390
+ * it anyone holding an id could read a TRASHED file's tags and the non-empty answer
2391
+ * would confirm the file exists. Same rule as `listPageTerms`, for the same reason. */
2392
+ listMediaTerms: import("@pramen/server").Handler<{
2393
+ mediaId: string;
2394
+ }, Term[]>;
2395
+ /** Replace a media asset's term assignments wholesale — set semantics, like
2396
+ * `setPageTerms`, and for the same reason: the panel holds the whole selection, and two
2397
+ * calls each patching one end of it race into a state neither asked for. */
2398
+ setMediaTerms: import("@pramen/server").Handler<{
2399
+ mediaId: string;
2400
+ termIds: string[];
2401
+ }, {
2402
+ ok: true;
2403
+ count: number;
2404
+ }>;
2228
2405
  /** Trash a media row. The R2 OBJECT IS KEPT — deleting the bytes here would make
2229
2406
  * `restoreMedia` a lie, and a block still referencing the id would render a dead url
2230
2407
  * with no way back. `purgeMedia` is what drops both — and `listTrash` is how you find
@@ -2386,6 +2563,7 @@ export declare const cmsHandlers: {
2386
2563
  pagesByType: true;
2387
2564
  siteFurniture: true;
2388
2565
  codeDefinedTypes: true;
2566
+ mediaTerms: true;
2389
2567
  canEdit: boolean;
2390
2568
  }>;
2391
2569
  /** Distinct locales present across all pages. NOTE: a DATA query — what is in the
@@ -2916,6 +3094,18 @@ export declare function createCollectionTasks(collections: readonly CollectionDe
2916
3094
  * between the revision insert and the page update leaves the page unpublished with an orphan
2917
3095
  * revision until the next at-least-once redelivery re-runs (the token still matches, so it
2918
3096
  * completes). Acceptable for a scheduled job; the interactive path is atomic. */
3097
+ /**
3098
+ * The CMS's own data migrations — spread into `app.migrations`.
3099
+ *
3100
+ * migrations: [...cmsMigrations]
3101
+ *
3102
+ * Opt-in like every other fragment this package ships (`cmsHandlers`, `cmsPolicies`,
3103
+ * `cmsTasks`), and with the same consequence for forgetting it: nothing breaks loudly. Media
3104
+ * rows written before the projection columns existed keep NULL `filename`/`contentType`, so
3105
+ * they sort together under a name sort and answer only the `other` type filter. New uploads
3106
+ * are unaffected — `createMedia` writes the columns itself.
3107
+ */
3108
+ export declare const cmsMigrations: readonly DataMigration[];
2919
3109
  export declare const cmsTasks: {
2920
3110
  "cms:publish": (ctx: HandlerContext, payload: unknown) => Promise<void>;
2921
3111
  "cms:unpublish": (ctx: HandlerContext, payload: unknown) => Promise<void>;