smoodly 0.0.45 → 0.0.47

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.
Files changed (245) hide show
  1. package/README.md +3 -3
  2. package/dist/addresses.d.ts +6 -7
  3. package/dist/addresses.js +6 -4
  4. package/dist/admin/PublicSans-LICENSE.txt +93 -0
  5. package/dist/admin/auth-config.d.ts +6 -0
  6. package/dist/admin/auth-config.js +1 -0
  7. package/dist/admin/browser/fetch-op.d.ts +10 -0
  8. package/dist/admin/browser/fetch-op.js +44 -0
  9. package/dist/admin/browser/main.d.ts +1 -0
  10. package/dist/admin/browser/main.js +21 -0
  11. package/dist/admin/custom-section.d.ts +1 -1
  12. package/dist/admin/drift.d.ts +1 -1
  13. package/dist/admin/editor/EditorView.js +14 -14
  14. package/dist/admin/editor/autosave.js +1 -1
  15. package/dist/admin/editor/canvas-address.d.ts +2 -0
  16. package/dist/admin/editor/canvas-address.js +8 -0
  17. package/dist/admin/editor/overlay-geometry.d.ts +1 -1
  18. package/dist/admin/editor/protocol.d.ts +18 -4
  19. package/dist/admin/editor/protocol.js +15 -1
  20. package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
  21. package/dist/admin/editor/useCanvasBridge.js +5 -6
  22. package/dist/admin/editor/useInlineSession.js +4 -2
  23. package/dist/admin/files.d.ts +11 -0
  24. package/dist/admin/files.generated.js +2 -0
  25. package/dist/admin/files.js +8 -0
  26. package/dist/admin/forms/useRefOptions.d.ts +2 -2
  27. package/dist/admin/forms/useRefOptions.js +2 -2
  28. package/dist/admin/forms/useRichtextEditor.js +4 -4
  29. package/dist/admin/handler.d.ts +40 -0
  30. package/dist/admin/handler.js +151 -0
  31. package/dist/admin/health.d.ts +4 -0
  32. package/dist/admin/index.d.ts +1 -1
  33. package/dist/admin/ops-impl.d.ts +7 -1
  34. package/dist/admin/ops-impl.js +112 -35
  35. package/dist/admin/ops.d.ts +3 -2
  36. package/dist/admin/page-data.d.ts +21 -0
  37. package/dist/admin/page-data.js +4 -0
  38. package/dist/admin/page.d.ts +6 -0
  39. package/dist/admin/page.js +35 -0
  40. package/dist/admin/serialize.js +7 -7
  41. package/dist/admin/shell/AdminApp.js +2 -2
  42. package/dist/admin/shell/ContentHealthPage.js +3 -1
  43. package/dist/admin/shell/EntryForm.js +1 -1
  44. package/dist/admin/shell/router.d.ts +3 -16
  45. package/dist/admin/shell/router.js +3 -39
  46. package/dist/admin/shell/session.d.ts +2 -5
  47. package/dist/admin/supabase-auth.js +1 -1
  48. package/dist/admin/tree-ops.d.ts +1 -1
  49. package/dist/admin/ui/fonts.d.ts +12 -0
  50. package/dist/admin/ui/fonts.js +22 -0
  51. package/dist/admin/ui/theme.d.ts +13 -4
  52. package/dist/admin/ui/theme.js +28 -157
  53. package/dist/assemble.d.ts +60 -0
  54. package/dist/assemble.js +359 -0
  55. package/dist/astro/cache.d.ts +34 -0
  56. package/dist/astro/cache.js +69 -0
  57. package/dist/astro/components/Custom.astro +21 -0
  58. package/dist/astro/components/Guarded.astro +24 -0
  59. package/dist/astro/components/Head.astro +26 -0
  60. package/dist/astro/components/Image.astro +20 -0
  61. package/dist/astro/components/RichText.astro +23 -0
  62. package/dist/astro/components/Section.astro +20 -0
  63. package/dist/astro/components/Video.astro +5 -0
  64. package/dist/astro/components/Zone.astro +19 -0
  65. package/dist/astro/components.d.ts +6 -0
  66. package/dist/astro/components.js +7 -0
  67. package/dist/astro/content-loaders.d.ts +40 -0
  68. package/dist/astro/content-loaders.js +100 -0
  69. package/dist/astro/content.d.ts +9 -0
  70. package/dist/astro/content.js +17 -0
  71. package/dist/astro/custom.d.ts +1 -0
  72. package/dist/astro/custom.js +3 -0
  73. package/dist/astro/defaults.d.ts +20 -0
  74. package/dist/astro/defaults.js +21 -0
  75. package/dist/astro/env-files.d.ts +1 -0
  76. package/dist/astro/env-files.js +21 -0
  77. package/dist/astro/head.d.ts +13 -0
  78. package/dist/astro/head.js +19 -0
  79. package/dist/astro/image.d.ts +27 -0
  80. package/dist/astro/image.js +29 -0
  81. package/dist/astro/index.d.ts +43 -0
  82. package/dist/astro/index.js +111 -0
  83. package/dist/astro/locale-paths.d.ts +30 -0
  84. package/dist/astro/locale-paths.js +38 -0
  85. package/dist/astro/middleware.d.ts +1 -0
  86. package/dist/astro/middleware.js +83 -0
  87. package/dist/astro/preview-document.d.ts +1 -0
  88. package/dist/astro/preview-document.js +18 -0
  89. package/dist/astro/preview.d.ts +10 -0
  90. package/dist/astro/preview.js +72 -0
  91. package/dist/astro/richtext.d.ts +15 -0
  92. package/dist/astro/richtext.js +78 -0
  93. package/dist/astro/routes/admin.d.ts +3 -0
  94. package/dist/astro/routes/admin.js +41 -0
  95. package/dist/astro/routes/page.astro +17 -0
  96. package/dist/astro/routes/preview.astro +11 -0
  97. package/dist/astro/runtime.d.ts +32 -0
  98. package/dist/astro/runtime.js +46 -0
  99. package/dist/astro/scope.d.ts +45 -0
  100. package/dist/astro/scope.js +103 -0
  101. package/dist/astro/server.d.ts +14 -0
  102. package/dist/astro/server.js +45 -0
  103. package/dist/astro/shell.d.ts +67 -0
  104. package/dist/astro/shell.js +63 -0
  105. package/dist/astro/style.d.ts +1 -0
  106. package/dist/astro/style.js +11 -0
  107. package/dist/astro/views-check.d.ts +3 -0
  108. package/dist/astro/views-check.js +19 -0
  109. package/dist/astro/zone.d.ts +39 -0
  110. package/dist/astro/zone.js +33 -0
  111. package/dist/bind.d.ts +19 -0
  112. package/dist/bind.js +34 -0
  113. package/dist/bridge/bridge.d.ts +9 -0
  114. package/dist/bridge/bridge.js +107 -0
  115. package/dist/bridge/entry.d.ts +1 -0
  116. package/dist/bridge/entry.js +5 -0
  117. package/dist/bridge/preview-pill.d.ts +18 -0
  118. package/dist/bridge/preview-pill.js +115 -0
  119. package/dist/cli/import.d.ts +3 -0
  120. package/dist/cli/import.js +10 -1
  121. package/dist/cli/load-collections.js +4 -3
  122. package/dist/cli/load-config.d.ts +4 -0
  123. package/dist/cli/load-config.js +26 -0
  124. package/dist/cli/main.d.ts +5 -1
  125. package/dist/cli/main.js +24 -6
  126. package/dist/cli/migrate.d.ts +13 -0
  127. package/dist/cli/migrate.js +30 -2
  128. package/dist/cli/rebuild.d.ts +23 -0
  129. package/dist/cli/rebuild.js +22 -0
  130. package/dist/cli/space-io.d.ts +15 -1
  131. package/dist/cli/space-io.js +14 -0
  132. package/dist/client.js +5 -0
  133. package/dist/config.d.ts +28 -20
  134. package/dist/config.js +71 -75
  135. package/dist/content/content-api.d.ts +1 -1
  136. package/dist/content/node.d.ts +2 -3
  137. package/dist/content/node.js +2 -1
  138. package/dist/crash-message.d.ts +1 -0
  139. package/dist/crash-message.js +8 -0
  140. package/dist/css.d.ts +4 -0
  141. package/dist/css.js +5 -2
  142. package/dist/custom/CustomSection.d.ts +1 -1
  143. package/dist/custom/CustomSection.js +4 -8
  144. package/dist/custom/css.js +2 -2
  145. package/dist/custom/index.d.ts +6 -10
  146. package/dist/custom/index.js +1 -2
  147. package/dist/custom/layout.d.ts +5 -0
  148. package/dist/custom/layout.js +14 -0
  149. package/dist/document-stamp.d.ts +7 -0
  150. package/dist/document-stamp.js +48 -0
  151. package/dist/entry-page.d.ts +3 -18
  152. package/dist/env.js +1 -1
  153. package/dist/global-id.js +2 -2
  154. package/dist/global.d.ts +5 -2
  155. package/dist/global.js +8 -5
  156. package/dist/image/next-config.d.ts +3 -0
  157. package/dist/image/next-config.js +12 -7
  158. package/dist/image/remote-pattern.d.ts +1 -0
  159. package/dist/image/remote-pattern.js +5 -0
  160. package/dist/index.d.ts +15 -17
  161. package/dist/index.js +10 -19
  162. package/dist/marks.d.ts +7 -0
  163. package/dist/marks.js +7 -0
  164. package/dist/next/admin-route.d.ts +7 -0
  165. package/dist/next/admin-route.js +28 -0
  166. package/dist/next/bridge.d.ts +1 -0
  167. package/dist/next/bridge.js +26 -0
  168. package/dist/next/canvas-boundary.d.ts +1 -1
  169. package/dist/next/canvas-slot.d.ts +1 -0
  170. package/dist/next/canvas-slot.js +4 -3
  171. package/dist/next/index.d.ts +2 -0
  172. package/dist/next/index.js +1 -0
  173. package/dist/next/page-renderer.d.ts +5 -1
  174. package/dist/next/page-renderer.js +19 -33
  175. package/dist/next/preview-pill-define.d.ts +1 -0
  176. package/dist/next/preview-pill-define.js +10 -0
  177. package/dist/next/preview-pill.d.ts +0 -3
  178. package/dist/next/preview-pill.js +7 -50
  179. package/dist/next/queries.d.ts +3 -28
  180. package/dist/next/queries.js +3 -90
  181. package/dist/next/reads.d.ts +2 -0
  182. package/dist/next/reads.js +18 -0
  183. package/dist/page-context.d.ts +89 -0
  184. package/dist/page-context.js +8 -0
  185. package/dist/pairing.d.ts +2 -22
  186. package/dist/project.d.ts +85 -0
  187. package/dist/project.js +228 -0
  188. package/dist/published.d.ts +115 -0
  189. package/dist/published.js +136 -0
  190. package/dist/react.d.ts +61 -0
  191. package/dist/react.js +17 -0
  192. package/dist/read-shapes.d.ts +1 -1
  193. package/dist/reads.d.ts +80 -0
  194. package/dist/reads.js +142 -0
  195. package/dist/refs.d.ts +1 -1
  196. package/dist/registration.d.ts +8 -0
  197. package/dist/registration.js +17 -0
  198. package/dist/render.d.ts +5 -83
  199. package/dist/render.js +5 -10
  200. package/dist/resolve.d.ts +1 -1
  201. package/dist/richtext/index.d.ts +6 -2
  202. package/dist/richtext/index.js +3 -2
  203. package/dist/richtext-href.d.ts +4 -0
  204. package/dist/richtext-href.js +17 -0
  205. package/dist/richtext-render.js +1 -14
  206. package/dist/schema-version.d.ts +1 -1
  207. package/dist/schema-version.js +1 -1
  208. package/dist/section-crash.d.ts +2 -2
  209. package/dist/section-crash.js +2 -7
  210. package/dist/section-wrapper.d.ts +3 -10
  211. package/dist/section-wrapper.js +3 -2
  212. package/dist/site.d.ts +40 -14
  213. package/dist/site.js +199 -284
  214. package/dist/smd-key.d.ts +9 -0
  215. package/dist/smd-key.js +1 -0
  216. package/dist/sql-space.d.ts +5 -0
  217. package/dist/sql-space.js +11 -5
  218. package/dist/sql.d.ts +2 -0
  219. package/dist/sql.js +127 -1
  220. package/dist/store.d.ts +1 -1
  221. package/dist/supabase-published-store.d.ts +45 -0
  222. package/dist/supabase-published-store.js +247 -0
  223. package/dist/supabase-store.d.ts +1 -1
  224. package/dist/toolset.d.ts +2 -4
  225. package/dist/toolset.js +6 -4
  226. package/dist/version.d.ts +1 -1
  227. package/dist/version.js +1 -1
  228. package/dist/view-props.d.ts +40 -0
  229. package/dist/view-props.js +1 -0
  230. package/dist/views.d.ts +36 -0
  231. package/dist/views.js +52 -0
  232. package/dist/zones.d.ts +7 -7
  233. package/dist/zones.js +7 -6
  234. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  235. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  236. package/migrations/20261008120000_smoodly_v10.sql +47 -0
  237. package/package.json +46 -19
  238. package/dist/admin/next/bridge.d.ts +0 -2
  239. package/dist/admin/next/bridge.js +0 -129
  240. package/dist/admin/next/create-admin.d.ts +0 -19
  241. package/dist/admin/next/create-admin.js +0 -44
  242. package/dist/admin/next/index.d.ts +0 -8
  243. package/dist/admin/next/index.js +0 -8
  244. package/dist/admin/next/op-handler.d.ts +0 -18
  245. package/dist/admin/next/op-handler.js +0 -31
package/dist/site.js CHANGED
@@ -1,22 +1,17 @@
1
- import { ARRAY_FIELD_TYPES, entryFieldsIn, schemaOf } from "./entry-store.js";
2
- import { buildPageTree, chainOf, entryAlternates, entryPathRows, entrySegment, pageAlternates, pagePath, } from "./paths.js";
3
- import { resolveFields, resolveFieldSets, resolveMedia, resolveTree } from "./resolve.js";
4
- import { elementSchemas } from "./richtext-walk.js";
5
- import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
6
- import { SMD_KEY } from "./section-wrapper.js";
7
- import { resolveStyleValues } from "./styles.js";
8
- import { resolveCustom } from "./custom/index.js";
9
- import { addressMap, codeAddresses, lastPart } from "./addresses.js";
10
- import { fullPath, servedAnchor } from "./anchors.js";
1
+ import { schemaOf } from "./entry-store.js";
2
+ import { entrySegment } from "./paths.js";
3
+ import { lastPart } from "./addresses.js";
11
4
  import { withDefaults } from "./field-defaults.js";
12
5
  import { modelKey } from "./model-key.js";
6
+ import { documentStamp } from "./document-stamp.js";
13
7
  import { SMOODLY_VERSION } from "./version.js";
14
8
  import { toEntryItem, toPageItem } from "./read-shapes.js";
15
- import { entrySeo, pageSeo, SEO_KEY } from "./seo.js";
9
+ import { pageSeo } from "./seo.js";
10
+ import { checkEntryQuery, createAssembler, pageFieldsOf, siteShape } from "./assemble.js";
11
+ import { collectionOrder } from "./collections.js";
12
+ import { createProjector } from "./project.js";
16
13
  /** The `page` prop for a loaded page. `href` is the app's seam from a
17
14
  * locale-relative path to a site URL; the default keeps CMS paths as URLs. */
18
- /** Field types whose stored value is an object: not filterable (memory and SQL would disagree). */
19
- const OBJECT_FIELD_TYPES = new Set(["image", "video", "file", "richtext", "object"]);
20
15
  export function pageContextOf(hit, href = (p) => p) {
21
16
  const withUrl = (l) => ({ ...l, url: href(l.path, hit.locale) });
22
17
  return {
@@ -32,264 +27,144 @@ export function pageContextOf(hit, href = (p) => p) {
32
27
  };
33
28
  }
34
29
  export function createSmoodlySite(options) {
35
- const locales = options.locales;
36
- const homePageSlug = options.homePageSlug ?? "home";
37
- const styles = resolveStyleValues(options.styles);
38
- const custom = resolveCustom(options.custom);
39
- const elements = elementSchemas(options.elements);
40
- // Code-owned addresses from the site's own registry (spec 2026-09-29 nested paths §3): a code-owned root's path.
41
- const codeOwned = codeAddresses({ locales, pages: options.pages, collections: options.collections });
42
- const addresses = addressMap(codeOwned);
43
- const pathOpts = { locales, homePageSlug, addresses };
44
- /** The same addresses as the path index sees them: an entry's stored part joined to its collection's (spec 2026-09-29 anchored paths §2). */
45
- const anchors = { addresses, homePageSlug, defaultLocale: locales.default };
46
- const mounted = options.collections.filter((c) => c.path !== undefined);
30
+ const shape = siteShape(options);
31
+ const { locales, pageSchemas, homePageSlug, styles, custom, codeOwned } = shape;
32
+ const assembler = createAssembler(options, shape);
47
33
  // Every cached read keys on it (spec 2026-09-30 tester findings §3): an upgrade or a model change starts a fresh cache.
48
34
  const cacheKey = `${SMOODLY_VERSION}:${modelKey({
49
35
  // Whole schemas: a collection's order, tree or title field changes a read as its fields do (functions drop out in the hash).
50
36
  collections: options.collections,
51
- globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, section: g.section })),
37
+ globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, settings: g.settings, section: g.section })),
52
38
  sections: options.sections.map((x) => ("schema" in x ? x.schema : x)),
53
39
  elements: (options.elements ?? []).map((x) => ("schema" in x ? x.schema : x)),
54
- pages: options.pages.map((x) => x.schema),
40
+ pages: pageSchemas,
55
41
  locales, homePageSlug, styles, custom,
56
42
  })}`;
57
- /** The resolver's fetcher, bound to a locale: the merged fields of that
58
- * locale — published ones (a versioned collection's snapshot) on the
59
- * public path, the live rows in draft mode. */
60
- const fetcher = (draft, locale) => (collection, ids) => options.stores().entries.getMany(collection, ids, locale, draft ? undefined : { status: "published" });
61
- /** The resolver's path lookup, bound to a locale: each EntryRef's address (spec 2026-09-29 content API contract §2). */
62
- const pathLookup = (locale) => (collection, ids) => options.stores().entries.pathsIn(collection, ids, locale);
63
- /** The media fetcher (spec 2026-09-08 §9) — absent when the site runs without an asset store. */
64
- const assetFetcher = () => {
65
- const assets = options.stores().assets;
66
- return assets ? (ids) => assets.getMany(ids) : undefined;
67
- };
68
- const sectionSettings = (name) => {
69
- const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
70
- return (s && ("schema" in s ? s.schema : s).settings) ?? {};
71
- };
72
- /** Visible to the public in a locale: present there, and a folder or a
73
- * published page. Draft mode sees every present node. Presence is the
74
- * locale row — nothing falls back (spec 2026-09-06 §3). */
75
- const visible = (node, locale, draft) => {
76
- const row = node.locales[locale];
77
- return row !== undefined && (draft || node.template === null || row.status === "published");
78
- };
79
- const hasPage = (node, locale, draft) => {
80
- const row = node.locales[locale];
81
- return node.template !== null && row !== undefined && (draft || row.status === "published");
82
- };
83
- const pageFields = (template) => {
84
- if (template === null)
85
- return {};
86
- const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
87
- return p?.schema.fields ?? {};
88
- };
89
- /** The version whose fields a link shows: the draft's in draft mode, else the published one. */
90
- const versionFor = (node, locale, draft) => {
91
- const row = node.locales[locale];
92
- if (!row || node.template === null)
93
- return null;
94
- return draft ? row.draftVersionId : row.publishedVersionId;
95
- };
96
- /** Raw page fields (no asset resolution) of many nodes in ONE store read
97
- * (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
98
- async function linkFields(nodes, locale, draft) {
99
- const { pages } = options.stores();
100
- const wanted = new Map(); // node id → version id
101
- for (const n of nodes) {
102
- const v = versionFor(n, locale, draft);
103
- if (v)
104
- wanted.set(n.id, v);
105
- }
106
- const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
107
- const out = new Map();
108
- for (const n of nodes) {
109
- const v = wanted.get(n.id);
110
- out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
111
- }
112
- return out;
113
- }
114
- /** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
115
- const link = (node, byId, locale, draft, fields) => ({
116
- title: node.locales[locale].title,
117
- path: pagePath(node, byId, locale, pathOpts),
118
- hasPage: hasPage(node, locale, draft),
119
- fields: fields.get(node.id) ?? {},
43
+ // What a delivery row is stamped with (DESIGN §3, the document stamp): the document format and the schema parts
44
+ // that reach a document, never the package version, so a release leaves current documents current.
45
+ const stamp = documentStamp(options);
46
+ const localeOf = (o) => o?.locale ?? locales.default;
47
+ const published = () => options.stores().published;
48
+ const projector = createProjector({
49
+ assembler,
50
+ stores: () => { const s = options.stores(); return { pages: s.pages, entries: s.entries, published: s.published }; },
51
+ collections: options.collections, globals: options.globals ?? [], sections: options.sections, elements: options.elements,
52
+ locales, model: stamp,
120
53
  });
121
- async function loadPage(id, locale, path, draft, version) {
122
- const { pages } = options.stores();
123
- const record = await pages.getPage(id);
124
- if (!record || record.template === null)
125
- return null;
126
- const row = record.locales[locale];
127
- if (!row)
128
- return null;
129
- let raw;
130
- if (version !== undefined) {
131
- // History: a stored version of THIS page in THIS locale, and only inside draft mode.
132
- const stored = draft ? await pages.getVersion(version) : null;
133
- raw = stored && stored.pageId === id && stored.locale === locale ? stored.tree : null;
134
- }
135
- else {
136
- raw = draft ? await pages.getDraftTree(id, locale) : row.status === "published" ? await pages.getPublishedTree(id, locale) : null;
54
+ // Delivery (spec 2026-10-07 delivery model §3–§4): outside draft mode a read takes the stored document when it is
55
+ // current — built by this model and not marked stale — and otherwise assembles that one document, stores it and serves it.
56
+ const current = (row) => row.model === stamp && !row.stale;
57
+ /** One writer of record (DESIGN §3, the document stamp): only the deployment whose stamp the space recorded rewrites
58
+ * rows; the first writer into an empty record records itself; every other deployment serves assembled documents
59
+ * and writes nothing. Read only when a heal is about to write, so a current page stays one call. A "no" is kept for
60
+ * the process (a later rebuild at this stamp is seen by the next process, which costs assembled reads, never wrong
61
+ * ones); a "yes" is read again on each heal, so a rebuild elsewhere stops this writer at its next heal. */
62
+ let yields = false;
63
+ async function mayWrite() {
64
+ if (yields)
65
+ return false;
66
+ // The record is derived like the rows: a read or write of it that fails is logged, and the page is served
67
+ // assembled with nothing written (spec 2026-10-07 §2 Failure), never failed on it.
68
+ try {
69
+ const recorded = await published().recordedModel();
70
+ if (recorded === null) {
71
+ await published().recordModel(stamp);
72
+ return true;
73
+ }
74
+ yields = recorded !== stamp;
75
+ return !yields;
137
76
  }
138
- if (!raw)
139
- return null;
140
- const tree = await resolveTree(raw, {
141
- sections: options.sections,
142
- collections: options.collections,
143
- globals: options.globals,
144
- elements: options.elements,
145
- fetch: fetcher(draft, locale), paths: pathLookup(locale),
146
- assets: assetFetcher(),
147
- page: { fields: pageFields(record.template) },
148
- custom,
149
- });
150
- const all = await pages.listPages();
151
- const index = new Map(all.map((n) => [n.id, n]));
152
- const byId = (x) => index.get(x);
153
- const chain = chainOf(record, byId);
154
- const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
155
- const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
156
- const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
157
- const children = kids.map((n) => link(n, byId, locale, draft, fields));
158
- // Alternates advertise PUBLISHED locales only; the editor (draft) sees every present
159
- // one. Derived from the tree already listed — no index lookup (spec 2026-09-15 §2).
160
- const alternates = pageAlternates(record, byId, pathOpts, (l) => draft || record.locales[l]?.status === "published");
161
- const withSeo = (options.pages ?? []).some((x) => x.schema.kind === "page" && x.schema.name === record.template && x.schema.seo === true);
162
- return {
163
- kind: "page",
164
- locale,
165
- record,
166
- template: record.template,
167
- tree,
168
- title: row.title,
169
- path,
170
- fields: withDefaults(tree.fields, pageFields(record.template)),
171
- ancestors,
172
- children,
173
- locales: alternates,
174
- ...(withSeo ? { seo: pageSeo(tree.meta, row.title) } : {}),
175
- };
176
- }
177
- async function loadEntry(id, locale, path, draft) {
178
- const { entries } = options.stores();
179
- for (const schema of mounted) {
180
- const record = await entries.get(schema.name, id);
181
- if (!record)
182
- continue;
183
- // No row, or a draft row outside draft mode, is a 404 for THIS locale.
184
- const row = record.locales[locale];
185
- if (!row)
186
- return null;
187
- if (!draft && row.status !== "published")
188
- return null;
189
- const raw = draft
190
- ? entryFieldsIn(record, locale)
191
- : (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
192
- if (!raw)
193
- return null;
194
- const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
195
- // Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
196
- // One record is in memory (a nested entry's parents are not), so the `paths` index
197
- // supplies the paths — the same single lookup as before (spec 2026-09-15 §2).
198
- const known = await entries.pathsOf(schema.name, id);
199
- const alternates = entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", (l) => known[l] ?? null);
200
- return {
201
- kind: "entry", locale, collection: schema.name, entry: { ...record, fields }, path, locales: alternates,
202
- ...(schema.seo ? { seo: entrySeo(fields, schema) } : {}),
203
- };
77
+ catch (e) {
78
+ console.error("smoodly: reading the space's recorded document stamp failed; served assembled, nothing written.", e);
79
+ return false;
204
80
  }
205
- return null;
206
81
  }
207
- const localeOf = (o) => o?.locale ?? locales.default;
208
- async function readEntries(collection, o = {}) {
82
+ const fresh = (row) => current(row) && row.document != null;
83
+ const itemOf = (doc) => toEntryItem(doc.entry, doc.entry.fields, { path: doc.path, locale: doc.locale, locales: doc.locales, ...(doc.seo ? { seo: doc.seo } : {}) });
84
+ /** A page row's answer: its document when current (null for a folder or a hidden node), else the healed one. */
85
+ const pageOf = async (row, id, locale) => row && current(row) ? row.document : projector.healPage(id, locale, { seen: row !== null, write: await mayWrite() });
86
+ /** An entry document is served at an address only when its collection has addresses. */
87
+ const served = (doc) => (doc && doc.path !== null ? doc : null);
88
+ const entryOf = async (row, id, locale) => row && fresh(row) ? row.document : projector.healEntry(id, locale, { seen: row !== null, write: await mayWrite() });
89
+ /** An entry document as the projector would write it, from an assembled read (the address-less shape, path from the item). */
90
+ const assembledEntry = (collection, locale) => (read) => ({
91
+ kind: "entry", locale, collection, entry: { ...read.record, fields: read.item.fields },
92
+ path: read.item.path, locales: read.item.locales, ...(read.item.seo ? { seo: read.item.seo } : {}),
93
+ });
94
+ /** The published documents of a collection's query. Stale rows are rebuilt in one batched projection and the
95
+ * query read once more (spec §4): a rebuild that finds an entry unpublished removes its row, so it drops out. */
96
+ async function deliveredEntries(collection, o) {
209
97
  const schema = schemaOf(options.collections, collection);
98
+ checkEntryQuery(schema, collection, o);
210
99
  const locale = localeOf(o);
211
- const draft = o.draft ?? false;
212
- const { entries } = options.stores();
213
- // A filter that silently matched nothing would be worse than an error (spec 2026-09-11 §3).
214
- for (const key of Object.keys(o.where ?? {})) {
215
- if (key === "slug") {
216
- throw new Error(`smoodly: getEntries("${collection}"): "slug" is not filterable — where filters fields; find an entry by its path instead.`);
217
- }
218
- if (key === SEO_KEY)
219
- throw new Error(`smoodly: getEntries("${collection}"): "${SEO_KEY}" is reserved — the SEO panel's values are not filterable.`);
220
- if (!(key in schema.fields))
221
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" is not a field of the collection.`);
222
- // The memory adapter's `===` never matches an object while SQL's `@>` matches a subset, so the two would disagree.
223
- if (OBJECT_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
224
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" holds an object — filter on a scalar field instead.`);
100
+ const read = () => published().entries(collection, {
101
+ locale, where: o.where, order: o.order ?? collectionOrder(schema), limit: o.limit, ids: o.ids,
102
+ ...(o.parent !== undefined ? { parent: o.parent } : {}),
103
+ });
104
+ let rows = await read();
105
+ const behind = rows.filter((r) => !fresh(r)).map((r) => r.id);
106
+ if (behind.length > 0) {
107
+ // Not the writer of record: the list assembled now, nothing written (DESIGN §3).
108
+ if (!(await mayWrite()))
109
+ return (await assembler.entries(collection, { ...o, locale, draft: false })).map(assembledEntry(collection, locale));
110
+ // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
111
+ try {
112
+ await projector.entries(behind, { cap: Infinity });
113
+ rows = await read();
225
114
  }
226
- // Any-of reads the text form of the whole field (PostgREST's `->>`), which for
227
- // an array is its JSON — `["a"]` never matches "a" — so refuse rather than match nothing.
228
- if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
229
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
115
+ catch (e) {
116
+ console.error(`smoodly: rebuilding ${behind.length} ${collection} documents in the delivery tables failed; served as read.`, e);
230
117
  }
231
118
  }
232
- if (o.limit !== undefined && (!Number.isInteger(o.limit) || o.limit < 0)) {
233
- throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
234
- }
235
- // The store filters, orders and limits over the snapshot shown (spec 2026-09-29 content API contract §5): the
236
- // locale gate — presence in draft mode, published otherwise — is part of the query, so `limit` counts rows that are really there.
237
- const rows = await entries.query(collection, {
238
- locale, draft, where: o.where, order: o.order, limit: o.limit, ids: o.ids,
239
- ...(o.parent !== undefined ? { parent: o.parent } : {}),
240
- });
241
- // Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
242
- const all = schema.tree ? await entries.list(collection) : rows.map((r) => r.record);
243
- // References and media resolve for the whole list in one pass (§2, §6).
244
- const sets = rows.map((r) => ({ values: structuredClone(r.fields), descriptors: schema.fields }));
245
- await resolveFieldSets(sets, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
246
- const index = new Map(all.map((e) => [e.id, e]));
247
- const byId = (x) => index.get(x);
248
- return rows.map(({ record }, i) => {
249
- const paths = entryPathRows([record], byId, schema.name);
250
- const pathIn = (l) => {
251
- const row = paths.find((r) => r.locale === l);
252
- return row ? fullPath(anchors, row.anchor ?? "", row.path, l) : null;
253
- };
254
- const item = toEntryItem(record, sets[i].values, {
255
- path: pathIn(locale),
256
- locale,
257
- locales: entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", pathIn),
258
- ...(schema.seo ? { seo: entrySeo(sets[i].values, schema) } : {}),
259
- });
260
- return { record, item };
261
- });
119
+ return rows.flatMap((r) => (r.document != null ? [r.document] : []));
262
120
  }
263
- const getEntries = async (collection, o = {}) => (await readEntries(collection, o)).map((r) => r.item);
121
+ const getEntries = async (collection, o = {}) => o.draft ? (await assembler.entries(collection, o)).map((r) => r.item) : (await deliveredEntries(collection, o)).map(itemOf);
122
+ /** Every entry of the collection in the locale, with the record a tree and a bare slug read `parentId` from. */
123
+ const readAll = async (collection, locale, draft) => draft
124
+ ? assembler.entries(collection, { locale, draft })
125
+ : (await deliveredEntries(collection, { locale })).map((doc) => ({ record: doc.entry, item: itemOf(doc) }));
264
126
  const api = {
265
127
  async getPagesTree(o) {
266
128
  const locale = localeOf(o);
267
- const draft = o?.draft ?? false;
268
- const all = await options.stores().pages.listPages();
269
- const shown = all.filter((n) => visible(n, locale, draft));
270
- const index = new Map(shown.map((n) => [n.id, n]));
271
- const fields = await linkFields(shown, locale, draft);
272
- // Every node the walk reaches is present with a present chain. Left
273
- // out, with its subtree: a record no longer in code (no path, spec
274
- // 2026-09-29 nested paths §5), and a page where another code address
275
- // wins, which visitors never reach (anchored paths §4).
276
- const hidden = (node) => node.path === null || servedAnchor(anchors, locale, node.path) !== (chainOf(node.record, (x) => index.get(x))[0].fixed ?? "");
277
- const toItems = (nodes) => nodes.flatMap((node) => hidden(node) ? [] : [{
278
- id: node.record.id,
279
- title: node.record.locales[locale].title,
280
- path: node.path,
281
- hasPage: hasPage(node.record, locale, draft),
282
- fields: fields.get(node.record.id) ?? {},
283
- children: toItems(node.children),
284
- }]);
285
- return toItems(buildPageTree(shown, locale, pathOpts));
129
+ if (o?.draft)
130
+ return assembler.tree(locale, { draft: true });
131
+ // One light read. A row behind the model or marked stale is rebuilt before answering (spec §4), and the tree read
132
+ // again; a rebuild that finds the node gone or unpublished removes its row.
133
+ let rows = await published().tree(locale);
134
+ const behind = rows.filter((r) => !current(r)).map((r) => r.id);
135
+ if (behind.length > 0) {
136
+ // Not the writer of record: the tree assembled now, nothing written (DESIGN §3).
137
+ if (!(await mayWrite()))
138
+ return assembler.tree(locale, { draft: false });
139
+ // One batched projection for every row behind (one tree read in the projector), then one more tree read.
140
+ // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
141
+ try {
142
+ await projector.pages(behind, { cap: Infinity });
143
+ rows = await published().tree(locale);
144
+ }
145
+ catch (e) {
146
+ console.error(`smoodly: rebuilding ${behind.length} page documents in the delivery tables failed; served as read.`, e);
147
+ }
148
+ }
149
+ // A node the tree hides (path null) leaves with its subtree, as does one whose parent has no row.
150
+ const build = (parentId) => rows.filter((r) => r.parentId === parentId && r.path !== null).map((r) => ({
151
+ id: r.id, title: r.title, path: r.path, hasPage: r.hasPage, fields: r.linkFields, children: build(r.id),
152
+ }));
153
+ return build(null);
286
154
  },
287
155
  async getPage(pathOrId, o) {
288
156
  const locale = localeOf(o);
157
+ if (!o?.draft) {
158
+ // The content API stays strict (§7): a redirect row, and a code-owned page with no record, are not pages.
159
+ const hit = pathOrId.startsWith("/")
160
+ ? (await delivered(locale, pathOrId, false)).hit
161
+ : await pageOf(await published().page(pathOrId, locale), pathOrId, locale);
162
+ return hit?.kind === "page" ? toPageItem(hit) : null;
163
+ }
289
164
  const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
290
165
  if (!path)
291
166
  return null;
292
- const hit = await site.resolve(locale, path, { draft: o?.draft ?? false });
167
+ const hit = await site.resolve(locale, path, { draft: true });
293
168
  return hit?.kind === "page" ? toPageItem(hit) : null;
294
169
  },
295
170
  getEntries,
@@ -298,64 +173,79 @@ export function createSmoodlySite(options) {
298
173
  const locale = localeOf(o);
299
174
  const draft = o?.draft ?? false;
300
175
  if (idOrPath.startsWith("/")) {
301
- const hit = await site.resolve(locale, idOrPath, { draft });
176
+ const hit = draft ? await site.resolve(locale, idOrPath, { draft }) : (await delivered(locale, idOrPath, false)).hit;
302
177
  return hit?.kind === "entry" && hit.collection === collection ? toEntryItem(hit.entry, hit.entry.fields, hit) : null;
303
178
  }
304
- const [byId] = await readEntries(collection, { locale, draft, ids: [idOrPath] });
305
- if (byId)
306
- return byId.item;
179
+ if (draft) {
180
+ const [byId] = await assembler.entries(collection, { locale, draft, ids: [idOrPath] });
181
+ if (byId)
182
+ return byId.item;
183
+ }
184
+ else {
185
+ const row = await published().entry(idOrPath, locale);
186
+ if (row && row.collection === collection && fresh(row))
187
+ return itemOf(row.document);
188
+ // A miss is a record no projection wrote yet, or a slug: one `get` tells them apart.
189
+ if (row?.collection === collection || (await options.stores().entries.get(collection, idOrPath))) {
190
+ const doc = await projector.healEntry(idOrPath, locale, { seen: row !== null, write: await mayWrite() });
191
+ if (doc)
192
+ return itemOf(doc);
193
+ }
194
+ }
307
195
  // A bare slug names a ROOT entry only (sibling-scoped slugs, spec §4):
308
196
  // match the localized slug directly (paths.ts owns slug derivation),
309
197
  // regardless of whether the collection even has a `path`.
310
- const all = await readEntries(collection, { locale, draft });
198
+ const all = await readAll(collection, locale, draft);
311
199
  return all.find((r) => r.record.parentId === null && entrySegment(r.record, locale) === idOrPath)?.item ?? null;
312
200
  },
313
201
  async getEntriesTree(collection, o) {
314
- const rows = await readEntries(collection, { locale: localeOf(o), draft: o?.draft ?? false });
202
+ const rows = await readAll(collection, localeOf(o), o?.draft ?? false);
315
203
  const build = (parentId) => rows.filter((r) => r.record.parentId === parentId).map((r) => ({ ...r.item, children: build(r.record.id) }));
316
204
  return build(null);
317
205
  },
318
206
  async getGlobal(item, o) {
319
207
  const locale = localeOf(o);
320
- const draft = o?.draft ?? false;
321
- const { entries } = options.stores();
322
- const [record] = await entries.list(item.name);
323
- if (!record)
324
- return null;
325
- const row = record.locales[locale];
326
- if (!row || (!draft && row.status !== "published"))
327
- return null;
328
- const raw = draft
329
- ? entryFieldsIn(record, locale)
330
- : (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
331
- if (!raw)
332
- return null;
333
- const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
334
- if (!item.section)
335
- return { locale, fields };
336
- const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
337
- const fetch = assetFetcher();
338
- const own = settings !== null && typeof settings === "object" ? settings : {};
339
- if (fetch)
340
- await resolveMedia(own, sectionSettings(item.section), fetch, elements);
341
- // The layout node id, the css and the values ride the styles under the reserved key (spec 2026-09-13 §1, 2026-09-14 §2, §4):
342
- // the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
343
- const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
344
- return {
345
- locale,
346
- fields: rest,
347
- settings: own,
348
- styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
349
- };
208
+ if (o?.draft)
209
+ return assembler.global(item, locale, { draft: true });
210
+ // The item's row by its collection, in one read: a global is a one-record collection.
211
+ const [row] = await published().entries(item.name, { locale, limit: 1 });
212
+ return row && fresh(row) ? row.document : projector.healGlobal(item, locale, { seen: row !== undefined, write: await mayWrite() });
350
213
  },
351
214
  };
215
+ /** The published read of an address; `code`: the code-owned page answers where the index has no row. */
216
+ /** `globals`: the names of global items to bring along (schema v9); `rows` is what the route carried, when it had any. */
217
+ async function delivered(locale, path, code, globals) {
218
+ const route = await published().byAddress(locale, path, globals);
219
+ const rows = route?.globals;
220
+ if (route && fresh(route.row))
221
+ return { hit: route.row.document, rows };
222
+ let healed = null;
223
+ if (route) {
224
+ // Stale: rebuilt by id. Unpublished or moved since the row was written, the address is answered as a miss.
225
+ const write = await mayWrite();
226
+ healed = route.kind === "page" ? await projector.healPage(route.row.id, locale, { write }) : served(await projector.healEntry(route.row.id, locale, { write }));
227
+ if (healed && healed.path === path)
228
+ return { hit: healed, rows };
229
+ }
230
+ // A miss: the paths index doubles as the existence check — a record to assemble, a redirect row, or nothing.
231
+ // A record wins over the code-owned page at its address, as the renderer has it.
232
+ const hit = await site.locate(locale, path);
233
+ if (!hit)
234
+ return { hit: code ? codePage(locale, path) : null };
235
+ if (hit.redirect)
236
+ return { hit: { kind: "redirect", target: { kind: hit.kind, id: hit.id } } };
237
+ if (route && hit.id === route.row.id)
238
+ return { hit: healed, rows }; // just rebuilt
239
+ // By id: a page the tree hides has a current row the address lookup cannot reach; anything else heals there.
240
+ return { hit: await site.load(hit, locale, path), rows };
241
+ }
352
242
  const codePath = (address) => (address === homePageSlug ? "/" : `/${address}`);
353
243
  function codePage(locale, path) {
354
244
  const spec = codeOwned.find((a) => a.template !== null && a.segments[locale] !== undefined && codePath(a.segments[locale]) === path);
355
245
  if (!spec || !locales.supported.includes(locale))
356
246
  return null;
357
247
  const template = spec.template;
358
- const withSeo = options.pages.some((x) => x.schema.kind === "page" && x.schema.name === template && x.schema.seo === true);
248
+ const withSeo = pageSchemas.some((x) => x.kind === "page" && x.name === template && x.seo === true);
359
249
  return {
360
250
  kind: "page",
361
251
  locale,
@@ -365,7 +255,7 @@ export function createSmoodlySite(options) {
365
255
  tree: { zones: {} },
366
256
  title: spec.title,
367
257
  path,
368
- fields: withDefaults(undefined, pageFields(template)),
258
+ fields: withDefaults(undefined, pageFieldsOf(shape, template)),
369
259
  ancestors: [],
370
260
  children: [],
371
261
  locales: Object.fromEntries(Object.entries(spec.segments).map(([l, address]) => [l, { slug: lastPart(address), path: codePath(address) }])),
@@ -376,13 +266,37 @@ export function createSmoodlySite(options) {
376
266
  locales,
377
267
  homePageSlug,
378
268
  cacheKey,
269
+ stamp,
379
270
  styles,
380
271
  custom,
381
272
  locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
382
273
  targetPaths: (target) => options.stores().pages.targetPaths(target),
383
- load(target, locale, path, opts = {}) {
274
+ async load(target, locale, path, opts = {}) {
384
275
  const draft = opts.draft ?? false;
385
- return target.kind === "page" ? loadPage(target.id, locale, path, draft, opts.version) : loadEntry(target.id, locale, path, draft);
276
+ // Draft mode assembles from the editing tables; so does a stored version, which outside draft mode is nothing.
277
+ if (draft || opts.version !== undefined) {
278
+ return target.kind === "page" ? assembler.page(target.id, locale, path, { draft, version: opts.version }) : assembler.entry(target.id, locale, path, { draft });
279
+ }
280
+ return target.kind === "page"
281
+ ? pageOf(await published().page(target.id, locale), target.id, locale)
282
+ : served(await entryOf(await published().entry(target.id, locale), target.id, locale));
283
+ },
284
+ lookup: async (locale, path) => (await delivered(locale, path, true)).hit,
285
+ async lookupWithGlobals(locale, path, items) {
286
+ const { hit, rows } = await delivered(locale, path, true, items.map((i) => i.name));
287
+ const globals = new Map();
288
+ if (rows) {
289
+ for (const item of items) {
290
+ const row = rows.find((r) => r.collection === item.name);
291
+ // No row: unpublished here, or never projected — left out rather than healed on every read, because a
292
+ // heal of an unpublished item writes nothing and the next read would heal again; the layout's own read
293
+ // pays the call where a page renders it. A stale row heals once and is written.
294
+ if (!row)
295
+ continue;
296
+ globals.set(item.name, fresh(row) ? row.document : await projector.healGlobal(item, locale, { seen: true, write: await mayWrite() }));
297
+ }
298
+ }
299
+ return { hit, globals };
386
300
  },
387
301
  async resolve(locale, path, opts) {
388
302
  // The content API stays strict (§7): a content address is not a redirect.
@@ -390,6 +304,7 @@ export function createSmoodlySite(options) {
390
304
  return target && !target.redirect ? site.load(target, locale, path, opts) : null;
391
305
  },
392
306
  codePage,
307
+ projector,
393
308
  ...api,
394
309
  };
395
310
  return site;
@@ -0,0 +1,9 @@
1
+ import type { StyleValues } from "./styles.ts";
2
+ export declare const SMD_KEY = "__smd";
3
+ export type SmdInternal = {
4
+ node?: string;
5
+ css?: string;
6
+ values?: StyleValues;
7
+ /** Pre-scoped layout CSS (the custom section, spec 2026-09-14 §6), emitted before the editor's css so the box wins. */
8
+ layoutCss?: string;
9
+ };
@@ -0,0 +1 @@
1
+ export const SMD_KEY = "__smd";
@@ -6,6 +6,11 @@ export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
6
6
  export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
7
7
  export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
8
8
  export type ContentTable = (typeof CONTENT_TABLES)[number];
9
+ /** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
10
+ * space-stamped like content, but not transferred (export skips them, import
11
+ * rebuilds them) and not counted toward the row cap, since they are derived.
12
+ * `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current. */
13
+ export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries", "published_models"];
9
14
  export declare function currentSpaceSQL(): string;
10
15
  /** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
11
16
  * the write key's insert, update and delete, each alone for its action; its