@pramen/cms 0.0.61 → 0.0.63
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 +34 -0
- package/dist/index.d.ts +196 -10
- package/dist/index.js +411 -9
- package/package.json +2 -2
- package/src/index.ts +435 -25
package/README.md
CHANGED
|
@@ -126,6 +126,34 @@ entry, so doing it there made the returned array stop matching the `as const` li
|
|
|
126
126
|
the tenant's `media/` prefix); the client PUTs the bytes, then `createMedia({ ref, alt? })`
|
|
127
127
|
confirms the blob is in R2 and persists a `cms_media` row. `listMedia`/`getMedia`/`deleteMedia`
|
|
128
128
|
(deleteMedia also removes the R2 blob) round it out. Editor-gated.
|
|
129
|
+
- **Browsing:** `listMedia({ limit, offset, q?, sort?, kind?, term? })`. `q` matches the filename **or**
|
|
130
|
+
the alt text (case-insensitive; a `%` or `_` in the needle is a literal), `sort` is one of
|
|
131
|
+
`newest`/`oldest`/`name`/`name_desc`/`largest`/`smallest`, and `kind` narrows to
|
|
132
|
+
`image`/`video`/`audio`/`document`/`other`. All three are applied in SQL rather than to the
|
|
133
|
+
page that arrived, so they mean what they say on a library larger than one page. `sort` and
|
|
134
|
+
`kind` are closed vocabularies — a caller never names a column — and an unrecognised value
|
|
135
|
+
falls back to the default instead of erroring.
|
|
136
|
+
|
|
137
|
+
This is what `cms_media.filename`/`contentType`/`size` are for: `file` is a `fileRef` (JSON in
|
|
138
|
+
a TEXT cell), which `orderBy` and `where` cannot see into, so the three fields the library
|
|
139
|
+
queries by are projected onto indexed columns when a row is created. **Spread `cmsMigrations`
|
|
140
|
+
into `app.migrations`** to backfill rows written before those columns existed — without it
|
|
141
|
+
they keep NULL, sort together under a name sort, and answer only the `other` filter.
|
|
142
|
+
- **Tagging:** files carry taxonomy terms from the same `cms_taxonomies`/`cms_terms` tables pages
|
|
143
|
+
use, through a `cms_media_terms` junction — one vocabulary, edited in one place, applied to
|
|
144
|
+
whichever of the two it declares. A vocabulary carries **`appliesTo`** — `["page"]`,
|
|
145
|
+
`["media"]`, both, or `null` for everything (which is what an un-narrowed one, and every row
|
|
146
|
+
written before the column existed, means). `listTaxonomies({ target })` narrows to it, and
|
|
147
|
+
`setPageTerms`/`setMediaTerms` REFUSE a term from a vocabulary that does not apply, so it is a
|
|
148
|
+
rule rather than a UI hint. Narrowing a vocabulary away from something it is still assigned to
|
|
149
|
+
is refused too — those assignments would stay stored and stop being reachable from the panel
|
|
150
|
+
that could remove them. `listMediaTerms({ mediaId })` reads a file's terms and `setMediaTerms({ mediaId, termIds })`
|
|
151
|
+
replaces them wholesale (set semantics, like `setPageTerms`); `listMedia({ term })` filters by
|
|
152
|
+
one, ANDed with `kind` and `q`. The filter is a relation traversal compiled to a subquery, so
|
|
153
|
+
it narrows in SQL like every other option here. Deleting a term takes its assignments with it
|
|
154
|
+
(a real `ON DELETE CASCADE`), and trashing a file does NOT — only `purgeMedia` does, so a
|
|
155
|
+
restored file keeps its tags. Declared to the editor as
|
|
156
|
+
`listCmsCapabilities().mediaTerms`.
|
|
129
157
|
- **Reference from a block:** a `"media"` field stores a `cms_media` id. At assemble/publish time
|
|
130
158
|
the id is resolved (recursively, through group/repeater nesting) to a `ResolvedMedia`
|
|
131
159
|
`{ id, key, url, alt, contentType, filename }` in the snapshot — so the content API returns a
|
|
@@ -195,6 +223,12 @@ authorization. Spread `cmsRoutes()` into `app.routes` to serve `GET /cms/preview
|
|
|
195
223
|
verifies the token in the Worker before any read and returns the live draft with
|
|
196
224
|
`isPreview: true` and `Cache-Control: private, no-store`.
|
|
197
225
|
|
|
226
|
+
That route answers with **JSON** — this CMS is headless, so it has the draft and no idea what
|
|
227
|
+
it should look like. Your site renders it: redeem the same token with `client.getPreview(token)`
|
|
228
|
+
(`@pramen/cms-astro`) from a route of your own, through the same components the published page
|
|
229
|
+
uses, and point the editor's Preview link button at it with `admin.previewUrl`. A working one
|
|
230
|
+
is `example/site/src/pages/preview.astro`.
|
|
231
|
+
|
|
198
232
|
If you pass custom roles to `createCmsHandlers`, hand `cmsRoutes` the **same options
|
|
199
233
|
object** — it derives the route's identity from them, so the two cannot drift:
|
|
200
234
|
|
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,8 +1117,36 @@ export declare const cmsSchema: {
|
|
|
1059
1117
|
} & {
|
|
1060
1118
|
readonly defaultExpr: string;
|
|
1061
1119
|
};
|
|
1062
|
-
},
|
|
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
1152
|
export { adminPage, createAdminPageHandlers, normalizeAdminResponse, validateAdminPages, MAX_ADMIN_BLOCK_DEPTH, } from "./blockkit";
|
|
@@ -1478,28 +1564,48 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
1478
1564
|
}, {
|
|
1479
1565
|
ok: true;
|
|
1480
1566
|
}>;
|
|
1481
|
-
/** Every vocabulary
|
|
1482
|
-
* is a URL segment) and a front end routes on
|
|
1483
|
-
|
|
1567
|
+
/** Every vocabulary, or just the ones that classify `target`. PUBLIC — like content
|
|
1568
|
+
* types, a taxonomy's slug is structural (it is a URL segment) and a front end routes on
|
|
1569
|
+
* it.
|
|
1570
|
+
*
|
|
1571
|
+
* Narrowed HERE rather than in each caller, so the page panel, the media panel and the
|
|
1572
|
+
* write-side guard cannot disagree about what a vocabulary applies to. In memory, because
|
|
1573
|
+
* `appliesTo` is a `t.json()` column that `where` cannot see into — which costs nothing:
|
|
1574
|
+
* this handler already reads every taxonomy, and nothing pages by them.
|
|
1575
|
+
*
|
|
1576
|
+
* No `target` means EVERY vocabulary, which is what the Taxonomies screen needs: the one
|
|
1577
|
+
* place that edits `appliesTo` must be able to see a vocabulary it has narrowed away. */
|
|
1578
|
+
listTaxonomies: import("@pramen/server").Handler<{
|
|
1579
|
+
target?: TaxonomyTarget;
|
|
1580
|
+
}, Record<string, unknown>[]>;
|
|
1484
1581
|
createTaxonomy: import("@pramen/server").Handler<{
|
|
1485
1582
|
slug: string;
|
|
1486
1583
|
label: string;
|
|
1487
1584
|
pluralLabel?: string;
|
|
1488
1585
|
description?: string;
|
|
1489
1586
|
hierarchical?: boolean;
|
|
1587
|
+
appliesTo?: TaxonomyTarget[] | null;
|
|
1490
1588
|
}, Record<string, unknown>>;
|
|
1491
1589
|
/** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
|
|
1492
1590
|
* is not mutable — the same rule content types and block types already follow.
|
|
1493
1591
|
*
|
|
1494
1592
|
* Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
|
|
1495
1593
|
* 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.
|
|
1594
|
+
* flattening the terms silently is a destructive edit behind a checkbox.
|
|
1595
|
+
*
|
|
1596
|
+
* NARROWING `appliesTo` is refused on exactly the same grounds, and it is the same bug:
|
|
1597
|
+
* dropping a target this vocabulary is already used for would strand those assignments —
|
|
1598
|
+
* still stored, still returned by `listPageTerms`/`listMediaTerms`, but invisible in the
|
|
1599
|
+
* panel that could remove them, because the panel only renders vocabularies that apply.
|
|
1600
|
+
* Unassign them first; then the narrowing is a settings change rather than a silent
|
|
1601
|
+
* orphaning. WIDENING is always fine — it strands nothing. */
|
|
1497
1602
|
updateTaxonomy: import("@pramen/server").Handler<{
|
|
1498
1603
|
id: string;
|
|
1499
1604
|
label?: string;
|
|
1500
1605
|
pluralLabel?: string | null;
|
|
1501
1606
|
description?: string | null;
|
|
1502
1607
|
hierarchical?: boolean;
|
|
1608
|
+
appliesTo?: TaxonomyTarget[] | null;
|
|
1503
1609
|
}, Record<string, unknown> | undefined>;
|
|
1504
1610
|
/** Delete a vocabulary. Its terms go with it, and their page assignments with those —
|
|
1505
1611
|
* both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
|
|
@@ -1617,6 +1723,10 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
1617
1723
|
listMedia: import("@pramen/server").Handler<{
|
|
1618
1724
|
limit?: number;
|
|
1619
1725
|
offset?: number;
|
|
1726
|
+
sort?: MediaSort;
|
|
1727
|
+
kind?: MediaKind;
|
|
1728
|
+
q?: string;
|
|
1729
|
+
term?: string;
|
|
1620
1730
|
}, Record<string, unknown>[]>;
|
|
1621
1731
|
getMedia: import("@pramen/server").Handler<{
|
|
1622
1732
|
id: string;
|
|
@@ -1626,6 +1736,25 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
1626
1736
|
id: string;
|
|
1627
1737
|
alt: string | null;
|
|
1628
1738
|
}, Record<string, unknown>>;
|
|
1739
|
+
/** A media asset's assigned terms.
|
|
1740
|
+
*
|
|
1741
|
+
* The media row is read FIRST, through `ctx.db`, so the caller's own scope decides
|
|
1742
|
+
* whether this answers — the junction and `cms_terms` are granted unscoped, so without
|
|
1743
|
+
* it anyone holding an id could read a TRASHED file's tags and the non-empty answer
|
|
1744
|
+
* would confirm the file exists. Same rule as `listPageTerms`, for the same reason. */
|
|
1745
|
+
listMediaTerms: import("@pramen/server").Handler<{
|
|
1746
|
+
mediaId: string;
|
|
1747
|
+
}, Term[]>;
|
|
1748
|
+
/** Replace a media asset's term assignments wholesale — set semantics, like
|
|
1749
|
+
* `setPageTerms`, and for the same reason: the panel holds the whole selection, and two
|
|
1750
|
+
* calls each patching one end of it race into a state neither asked for. */
|
|
1751
|
+
setMediaTerms: import("@pramen/server").Handler<{
|
|
1752
|
+
mediaId: string;
|
|
1753
|
+
termIds: string[];
|
|
1754
|
+
}, {
|
|
1755
|
+
ok: true;
|
|
1756
|
+
count: number;
|
|
1757
|
+
}>;
|
|
1629
1758
|
/** Trash a media row. The R2 OBJECT IS KEPT — deleting the bytes here would make
|
|
1630
1759
|
* `restoreMedia` a lie, and a block still referencing the id would render a dead url
|
|
1631
1760
|
* with no way back. `purgeMedia` is what drops both — and `listTrash` is how you find
|
|
@@ -1787,6 +1916,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
1787
1916
|
pagesByType: true;
|
|
1788
1917
|
siteFurniture: true;
|
|
1789
1918
|
codeDefinedTypes: true;
|
|
1919
|
+
mediaTerms: true;
|
|
1790
1920
|
canEdit: boolean;
|
|
1791
1921
|
}>;
|
|
1792
1922
|
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
@@ -2077,28 +2207,48 @@ export declare const cmsHandlers: {
|
|
|
2077
2207
|
}, {
|
|
2078
2208
|
ok: true;
|
|
2079
2209
|
}>;
|
|
2080
|
-
/** Every vocabulary
|
|
2081
|
-
* is a URL segment) and a front end routes on
|
|
2082
|
-
|
|
2210
|
+
/** Every vocabulary, or just the ones that classify `target`. PUBLIC — like content
|
|
2211
|
+
* types, a taxonomy's slug is structural (it is a URL segment) and a front end routes on
|
|
2212
|
+
* it.
|
|
2213
|
+
*
|
|
2214
|
+
* Narrowed HERE rather than in each caller, so the page panel, the media panel and the
|
|
2215
|
+
* write-side guard cannot disagree about what a vocabulary applies to. In memory, because
|
|
2216
|
+
* `appliesTo` is a `t.json()` column that `where` cannot see into — which costs nothing:
|
|
2217
|
+
* this handler already reads every taxonomy, and nothing pages by them.
|
|
2218
|
+
*
|
|
2219
|
+
* No `target` means EVERY vocabulary, which is what the Taxonomies screen needs: the one
|
|
2220
|
+
* place that edits `appliesTo` must be able to see a vocabulary it has narrowed away. */
|
|
2221
|
+
listTaxonomies: import("@pramen/server").Handler<{
|
|
2222
|
+
target?: TaxonomyTarget;
|
|
2223
|
+
}, Record<string, unknown>[]>;
|
|
2083
2224
|
createTaxonomy: import("@pramen/server").Handler<{
|
|
2084
2225
|
slug: string;
|
|
2085
2226
|
label: string;
|
|
2086
2227
|
pluralLabel?: string;
|
|
2087
2228
|
description?: string;
|
|
2088
2229
|
hierarchical?: boolean;
|
|
2230
|
+
appliesTo?: TaxonomyTarget[] | null;
|
|
2089
2231
|
}, Record<string, unknown>>;
|
|
2090
2232
|
/** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
|
|
2091
2233
|
* is not mutable — the same rule content types and block types already follow.
|
|
2092
2234
|
*
|
|
2093
2235
|
* Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
|
|
2094
2236
|
* 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.
|
|
2237
|
+
* flattening the terms silently is a destructive edit behind a checkbox.
|
|
2238
|
+
*
|
|
2239
|
+
* NARROWING `appliesTo` is refused on exactly the same grounds, and it is the same bug:
|
|
2240
|
+
* dropping a target this vocabulary is already used for would strand those assignments —
|
|
2241
|
+
* still stored, still returned by `listPageTerms`/`listMediaTerms`, but invisible in the
|
|
2242
|
+
* panel that could remove them, because the panel only renders vocabularies that apply.
|
|
2243
|
+
* Unassign them first; then the narrowing is a settings change rather than a silent
|
|
2244
|
+
* orphaning. WIDENING is always fine — it strands nothing. */
|
|
2096
2245
|
updateTaxonomy: import("@pramen/server").Handler<{
|
|
2097
2246
|
id: string;
|
|
2098
2247
|
label?: string;
|
|
2099
2248
|
pluralLabel?: string | null;
|
|
2100
2249
|
description?: string | null;
|
|
2101
2250
|
hierarchical?: boolean;
|
|
2251
|
+
appliesTo?: TaxonomyTarget[] | null;
|
|
2102
2252
|
}, Record<string, unknown> | undefined>;
|
|
2103
2253
|
/** Delete a vocabulary. Its terms go with it, and their page assignments with those —
|
|
2104
2254
|
* both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
|
|
@@ -2216,6 +2366,10 @@ export declare const cmsHandlers: {
|
|
|
2216
2366
|
listMedia: import("@pramen/server").Handler<{
|
|
2217
2367
|
limit?: number;
|
|
2218
2368
|
offset?: number;
|
|
2369
|
+
sort?: MediaSort;
|
|
2370
|
+
kind?: MediaKind;
|
|
2371
|
+
q?: string;
|
|
2372
|
+
term?: string;
|
|
2219
2373
|
}, Record<string, unknown>[]>;
|
|
2220
2374
|
getMedia: import("@pramen/server").Handler<{
|
|
2221
2375
|
id: string;
|
|
@@ -2225,6 +2379,25 @@ export declare const cmsHandlers: {
|
|
|
2225
2379
|
id: string;
|
|
2226
2380
|
alt: string | null;
|
|
2227
2381
|
}, Record<string, unknown>>;
|
|
2382
|
+
/** A media asset's assigned terms.
|
|
2383
|
+
*
|
|
2384
|
+
* The media row is read FIRST, through `ctx.db`, so the caller's own scope decides
|
|
2385
|
+
* whether this answers — the junction and `cms_terms` are granted unscoped, so without
|
|
2386
|
+
* it anyone holding an id could read a TRASHED file's tags and the non-empty answer
|
|
2387
|
+
* would confirm the file exists. Same rule as `listPageTerms`, for the same reason. */
|
|
2388
|
+
listMediaTerms: import("@pramen/server").Handler<{
|
|
2389
|
+
mediaId: string;
|
|
2390
|
+
}, Term[]>;
|
|
2391
|
+
/** Replace a media asset's term assignments wholesale — set semantics, like
|
|
2392
|
+
* `setPageTerms`, and for the same reason: the panel holds the whole selection, and two
|
|
2393
|
+
* calls each patching one end of it race into a state neither asked for. */
|
|
2394
|
+
setMediaTerms: import("@pramen/server").Handler<{
|
|
2395
|
+
mediaId: string;
|
|
2396
|
+
termIds: string[];
|
|
2397
|
+
}, {
|
|
2398
|
+
ok: true;
|
|
2399
|
+
count: number;
|
|
2400
|
+
}>;
|
|
2228
2401
|
/** Trash a media row. The R2 OBJECT IS KEPT — deleting the bytes here would make
|
|
2229
2402
|
* `restoreMedia` a lie, and a block still referencing the id would render a dead url
|
|
2230
2403
|
* with no way back. `purgeMedia` is what drops both — and `listTrash` is how you find
|
|
@@ -2386,6 +2559,7 @@ export declare const cmsHandlers: {
|
|
|
2386
2559
|
pagesByType: true;
|
|
2387
2560
|
siteFurniture: true;
|
|
2388
2561
|
codeDefinedTypes: true;
|
|
2562
|
+
mediaTerms: true;
|
|
2389
2563
|
canEdit: boolean;
|
|
2390
2564
|
}>;
|
|
2391
2565
|
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
@@ -2916,6 +3090,18 @@ export declare function createCollectionTasks(collections: readonly CollectionDe
|
|
|
2916
3090
|
* between the revision insert and the page update leaves the page unpublished with an orphan
|
|
2917
3091
|
* revision until the next at-least-once redelivery re-runs (the token still matches, so it
|
|
2918
3092
|
* completes). Acceptable for a scheduled job; the interactive path is atomic. */
|
|
3093
|
+
/**
|
|
3094
|
+
* The CMS's own data migrations — spread into `app.migrations`.
|
|
3095
|
+
*
|
|
3096
|
+
* migrations: [...cmsMigrations]
|
|
3097
|
+
*
|
|
3098
|
+
* Opt-in like every other fragment this package ships (`cmsHandlers`, `cmsPolicies`,
|
|
3099
|
+
* `cmsTasks`), and with the same consequence for forgetting it: nothing breaks loudly. Media
|
|
3100
|
+
* rows written before the projection columns existed keep NULL `filename`/`contentType`, so
|
|
3101
|
+
* they sort together under a name sort and answer only the `other` type filter. New uploads
|
|
3102
|
+
* are unaffected — `createMedia` writes the columns itself.
|
|
3103
|
+
*/
|
|
3104
|
+
export declare const cmsMigrations: readonly DataMigration[];
|
|
2919
3105
|
export declare const cmsTasks: {
|
|
2920
3106
|
"cms:publish": (ctx: HandlerContext, payload: unknown) => Promise<void>;
|
|
2921
3107
|
"cms:unpublish": (ctx: HandlerContext, payload: unknown) => Promise<void>;
|