@thomasfosterau/pulpit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1272 -2
  3. package/dist/blobs.d.ts +249 -0
  4. package/dist/blobs.d.ts.map +1 -0
  5. package/dist/blobs.js +193 -0
  6. package/dist/blobs.js.map +1 -0
  7. package/dist/budgets.d.ts +255 -0
  8. package/dist/budgets.d.ts.map +1 -0
  9. package/dist/budgets.js +211 -0
  10. package/dist/budgets.js.map +1 -0
  11. package/dist/cache.d.ts +154 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +118 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/collections.d.ts +242 -0
  16. package/dist/collections.d.ts.map +1 -0
  17. package/dist/collections.js +215 -0
  18. package/dist/collections.js.map +1 -0
  19. package/dist/comments.d.ts +212 -0
  20. package/dist/comments.d.ts.map +1 -0
  21. package/dist/comments.js +192 -0
  22. package/dist/comments.js.map +1 -0
  23. package/dist/content.d.ts +231 -0
  24. package/dist/content.d.ts.map +1 -0
  25. package/dist/content.js +204 -0
  26. package/dist/content.js.map +1 -0
  27. package/dist/documents.d.ts +184 -0
  28. package/dist/documents.d.ts.map +1 -0
  29. package/dist/documents.js +190 -0
  30. package/dist/documents.js.map +1 -0
  31. package/dist/edges.d.ts +645 -0
  32. package/dist/edges.d.ts.map +1 -0
  33. package/dist/edges.js +558 -0
  34. package/dist/edges.js.map +1 -0
  35. package/dist/filter.d.ts +461 -0
  36. package/dist/filter.d.ts.map +1 -0
  37. package/dist/filter.js +387 -0
  38. package/dist/filter.js.map +1 -0
  39. package/dist/index.d.ts +133 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +133 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/internal/filter-columns.d.ts +8 -0
  44. package/dist/internal/filter-columns.d.ts.map +1 -0
  45. package/dist/internal/filter-columns.js +43 -0
  46. package/dist/internal/filter-columns.js.map +1 -0
  47. package/dist/internal/list-order.d.ts +26 -0
  48. package/dist/internal/list-order.d.ts.map +1 -0
  49. package/dist/internal/list-order.js +92 -0
  50. package/dist/internal/list-order.js.map +1 -0
  51. package/dist/internal/owned-columns.d.ts +93 -0
  52. package/dist/internal/owned-columns.d.ts.map +1 -0
  53. package/dist/internal/owned-columns.js +98 -0
  54. package/dist/internal/owned-columns.js.map +1 -0
  55. package/dist/internal/plain-rows.d.ts +32 -0
  56. package/dist/internal/plain-rows.d.ts.map +1 -0
  57. package/dist/internal/plain-rows.js +32 -0
  58. package/dist/internal/plain-rows.js.map +1 -0
  59. package/dist/internal/reference-columns.d.ts +80 -0
  60. package/dist/internal/reference-columns.d.ts.map +1 -0
  61. package/dist/internal/reference-columns.js +102 -0
  62. package/dist/internal/reference-columns.js.map +1 -0
  63. package/dist/moderation.d.ts +392 -0
  64. package/dist/moderation.d.ts.map +1 -0
  65. package/dist/moderation.js +375 -0
  66. package/dist/moderation.js.map +1 -0
  67. package/dist/paths.d.ts +204 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +165 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/proposals.d.ts +523 -0
  72. package/dist/proposals.d.ts.map +1 -0
  73. package/dist/proposals.js +505 -0
  74. package/dist/proposals.js.map +1 -0
  75. package/dist/publication.d.ts +148 -0
  76. package/dist/publication.d.ts.map +1 -0
  77. package/dist/publication.js +183 -0
  78. package/dist/publication.js.map +1 -0
  79. package/dist/purge.d.ts +224 -0
  80. package/dist/purge.d.ts.map +1 -0
  81. package/dist/purge.js +151 -0
  82. package/dist/purge.js.map +1 -0
  83. package/dist/reactions.d.ts +246 -0
  84. package/dist/reactions.d.ts.map +1 -0
  85. package/dist/reactions.js +183 -0
  86. package/dist/reactions.js.map +1 -0
  87. package/dist/registry.d.ts +316 -0
  88. package/dist/registry.d.ts.map +1 -0
  89. package/dist/registry.js +305 -0
  90. package/dist/registry.js.map +1 -0
  91. package/dist/search.d.ts +366 -0
  92. package/dist/search.d.ts.map +1 -0
  93. package/dist/search.js +273 -0
  94. package/dist/search.js.map +1 -0
  95. package/dist/social.d.ts +311 -0
  96. package/dist/social.d.ts.map +1 -0
  97. package/dist/social.js +205 -0
  98. package/dist/social.js.map +1 -0
  99. package/dist/subtype.d.ts +1613 -0
  100. package/dist/subtype.d.ts.map +1 -0
  101. package/dist/subtype.js +1300 -0
  102. package/dist/subtype.js.map +1 -0
  103. package/dist/testing.d.ts +675 -0
  104. package/dist/testing.d.ts.map +1 -0
  105. package/dist/testing.js +1022 -0
  106. package/dist/testing.js.map +1 -0
  107. package/dist/version-chain.d.ts +702 -0
  108. package/dist/version-chain.d.ts.map +1 -0
  109. package/dist/version-chain.js +489 -0
  110. package/dist/version-chain.js.map +1 -0
  111. package/dist/working-copy.d.ts +116 -0
  112. package/dist/working-copy.d.ts.map +1 -0
  113. package/dist/working-copy.js +95 -0
  114. package/dist/working-copy.js.map +1 -0
  115. package/package.json +145 -3
  116. package/src/blobs.ts +384 -0
  117. package/src/budgets.ts +338 -0
  118. package/src/cache.ts +218 -0
  119. package/src/collections.ts +427 -0
  120. package/src/comments.ts +392 -0
  121. package/src/content.ts +238 -0
  122. package/src/documents.ts +284 -0
  123. package/src/edges.ts +1185 -0
  124. package/src/filter.ts +523 -0
  125. package/src/index.ts +153 -0
  126. package/src/internal/filter-columns.ts +50 -0
  127. package/src/internal/list-order.ts +117 -0
  128. package/src/internal/owned-columns.ts +140 -0
  129. package/src/internal/plain-rows.ts +54 -0
  130. package/src/internal/reference-columns.ts +156 -0
  131. package/src/moderation.ts +728 -0
  132. package/src/paths.ts +267 -0
  133. package/src/proposals.ts +1014 -0
  134. package/src/publication.ts +225 -0
  135. package/src/purge.ts +313 -0
  136. package/src/reactions.ts +347 -0
  137. package/src/registry.ts +524 -0
  138. package/src/search.ts +540 -0
  139. package/src/social.ts +469 -0
  140. package/src/subtype.ts +3095 -0
  141. package/src/testing.ts +1873 -0
  142. package/src/version-chain.ts +1093 -0
  143. package/src/working-copy.ts +192 -0
package/dist/paths.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * **Routable content** — a leaf's path claims, tied to its version chain,
3
+ * the slug aliases a path change leaves behind, and the gated resolution of
4
+ * a path to a node or a redirect.
5
+ *
6
+ * The behavioural spec is gardeneria's `apps/api/src/operations/pages.ts`
7
+ * and `packages/database/src/tables/pages.ts` (one live page per canonical
8
+ * path, old slugs kept in `page_slugs` and answered with a 301). A `#NNN`
9
+ * here names a gardeneria issue.
10
+ *
11
+ * ## The model (owner decision on pulpit#57)
12
+ *
13
+ * - **A claim lives on the root node**: one path per content item. The
14
+ * locale is chosen when the resolved node is read, never by the path, so a
15
+ * slug translation is not a claim of its own (pulpit#56's model (b)).
16
+ * - **Claims follow the node's lifecycle.** A write that leaves the node
17
+ * live claims the path its new state holds in the leaf's path column; a
18
+ * tombstone, or a state with no path, frees the claim; a restore re-claims
19
+ * its path, or fails {@link PathTaken} when another node holds it now.
20
+ * - **A path change records an alias**: the old path resolves as a redirect
21
+ * to the new one, for as long as no other node claims it.
22
+ * - **Resolution is gated** (`SEC-AUTH-ENUM-001`): {@link RoutableOps.resolve}
23
+ * reads the node through the leaf's own `get`, so a hidden node's path,
24
+ * and an alias to a hidden node, is {@link PathNotFound}, never a redirect.
25
+ *
26
+ * Free-form redirects (patterns, a 404 log) are the site's. The path's
27
+ * syntax is the leaf attribute's schema (effect-schema-ext's `Ids.slug`, once
28
+ * it publishes).
29
+ *
30
+ * @copyright Thomas Foster
31
+ * @module pulpit/paths
32
+ * @since 0.1.0
33
+ */
34
+ import * as Effect from "effect/Effect";
35
+ import * as Equivalence from "effect/Equivalence";
36
+ import * as Option from "effect/Option";
37
+ import * as Predicate from "effect/Predicate";
38
+ import * as Schema from "effect/Schema";
39
+ // ── The path-store seam ───────────────────────────────────────────────
40
+ /**
41
+ * Another node holds the path canonically: the wire's 409. It names the
42
+ * path, never the holder, so it says only that the path is taken, as a
43
+ * unique-key conflict does, whoever holds it.
44
+ *
45
+ * @since 0.1.0
46
+ */
47
+ export class PathTaken extends Schema.TaggedError()("PathTaken", {
48
+ path: Schema.String,
49
+ }) {
50
+ }
51
+ /**
52
+ * No node the viewer can read answers to the path: the wire's 404. A path
53
+ * nobody claims, one whose node is hidden from the viewer, and an alias to a
54
+ * hidden node all answer this, alike (`SEC-AUTH-ENUM-001`).
55
+ *
56
+ * @since 0.1.0
57
+ */
58
+ export class PathNotFound extends Schema.TaggedError()("PathNotFound", {
59
+ path: Schema.String,
60
+ }) {
61
+ }
62
+ /**
63
+ * Make a leaf routable (pulpit#57): its writes keep the node's path claim in
64
+ * step with its chain, and {@link RoutableOps.resolve} answers a path through
65
+ * the leaf's own read gate. After each write's append, in the same
66
+ * transaction, the claim follows the state the write landed at: a live state
67
+ * with a path claims it (a change leaves the old path as an alias; another
68
+ * node's claim fails {@link PathTaken}), and a tombstone or a state with no
69
+ * path frees it. A restore into a path another node took meanwhile therefore
70
+ * fails `PathTaken`, and its append rolls back with it. Wrap the result with
71
+ * `./subtype`'s `subtypeHandlers` as the leaf's own operations, so its
72
+ * `transact` covers the claims.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { strictEqual } from "node:assert";
77
+ * import * as Effect from "effect/Effect";
78
+ * import * as Option from "effect/Option";
79
+ * import { routable } from "@thomasfosterau/pulpit/paths";
80
+ * import { defineSubtype, nobodyFollows } from "@thomasfosterau/pulpit/subtype";
81
+ * import {
82
+ * makeInMemoryChainStore,
83
+ * makeInMemoryLeafStore,
84
+ * makeInMemoryPathStore,
85
+ * } from "@thomasfosterau/pulpit/testing";
86
+ * import * as HashSet from "effect/HashSet";
87
+ *
88
+ * const chain = makeInMemoryChainStore();
89
+ * const pages = routable(
90
+ * defineSubtype({
91
+ * type: "pages",
92
+ * access: { kind: "reference" },
93
+ * chain,
94
+ * leaves: makeInMemoryLeafStore(chain, { columns: ["slug"] }),
95
+ * viewer: Effect.succeed(Option.some({ id: "editor", role: "admin" })),
96
+ * policy: {
97
+ * canViewPrivate: () => true,
98
+ * canViewDeleted: () => true,
99
+ * canModerate: () => true,
100
+ * canWriteCorpus: () => true,
101
+ * },
102
+ * followers: nobodyFollows(HashSet.empty<string>()),
103
+ * toResource: (row) => ({ id: row.id, slug: row["slug"] }),
104
+ * }),
105
+ * { column: "slug", paths: makeInMemoryPathStore(), chain },
106
+ * );
107
+ * const program = Effect.gen(function* () {
108
+ * const { id } = yield* pages.create({ id: Option.none(), state: { slug: "about" } });
109
+ * yield* pages.update({ id, patch: { slug: "about-us" } });
110
+ * return yield* pages.resolve("about");
111
+ * });
112
+ * const resolved = Effect.runSync(program);
113
+ * strictEqual(resolved.kind === "redirect" && resolved.path, "about-us");
114
+ * ```
115
+ *
116
+ * @since 0.1.0
117
+ */
118
+ export const routable = (ops, config) => {
119
+ const { column, paths, chain } = config;
120
+ // The claim the written state calls for: its path while it is live, none
121
+ // for a tombstone or a state whose path column holds no string.
122
+ const follow = ({ id, version }) => Effect.gen(function* () {
123
+ const written = yield* chain.snapshot(id, version);
124
+ const path = Option.flatMap(written, (snapshot) => snapshot.action === "delete"
125
+ ? Option.none()
126
+ : Option.liftPredicate(snapshot.state[column], Predicate.isString));
127
+ const held = yield* paths.canonicalOf(id);
128
+ if (Option.makeEquivalence(Equivalence.String)(path, held))
129
+ return;
130
+ yield* Option.match(path, {
131
+ onNone: () => paths.release(id),
132
+ onSome: (claimed) => paths.claim(id, claimed),
133
+ });
134
+ });
135
+ const claiming = (write) => Effect.tap(write, follow);
136
+ const resolve = Effect.fn(`${ops.type}.resolve`)(function* (path) {
137
+ const notFound = new PathNotFound({ path });
138
+ const entry = yield* paths.lookup(path);
139
+ if (Option.isNone(entry))
140
+ return yield* notFound;
141
+ const { nodeId, canonical } = entry.value;
142
+ // The leaf's own gate: a hidden node answers exactly as no node.
143
+ const resource = yield* ops
144
+ .get(nodeId)
145
+ .pipe(Effect.catchTag("NodeNotFound", () => Effect.fail(notFound)));
146
+ if (canonical)
147
+ return { kind: "node", id: nodeId, resource };
148
+ const target = yield* paths.canonicalOf(nodeId);
149
+ return yield* Option.match(target, {
150
+ onNone: () => Effect.fail(notFound),
151
+ onSome: (to) => Effect.succeed({ kind: "redirect", id: nodeId, path: to }),
152
+ });
153
+ });
154
+ return {
155
+ ...ops,
156
+ create: (input) => claiming(ops.create(input)),
157
+ update: (input) => claiming(ops.update(input)),
158
+ restore: (input) => claiming(ops.restore(input)),
159
+ // A delete over a tombstone wrote nothing, and its claim is already free.
160
+ delete: (id, precondition) => Effect.tap(ops.delete(id, precondition), (written) => written.alreadyDeleted ? Effect.void : paths.release(id)),
161
+ restoreVersion: (id, version, precondition) => claiming(ops.restoreVersion(id, version, precondition)),
162
+ resolve,
163
+ };
164
+ };
165
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}