foldkit-cms-drizzle 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  The server half of [`foldkit-cms`](../cms/README.md), over
4
4
  [`foldkit-remote-drizzle`](../remote-drizzle/README.md). It keeps an author's
5
5
  unpublished work in three tables beside your own, and it is where the **audience
6
- boundary** is enforced: who is not an author is refused entries, drafts and
7
- revisions outright, and sees of your content only what is published.
6
+ boundary** is enforced, by a [`foldkit-guard`](../guard/README.md) guard set:
7
+ who is not an author is refused entries, drafts and revisions outright, and sees
8
+ of your content only what its Read rules show, such as what is published.
8
9
 
9
10
  Supports SQLite and Postgres. The conflict check requires `returning`, so this
10
11
  adapter does not support MySQL. The [CMS example](../../examples/cms/README.md)
@@ -17,8 +18,8 @@ includes the server, in-app preview, and a browser mode over an in-memory databa
17
18
  | Published content | your tables, bound with `bind` as any are |
18
19
  | Unpublished work | `cms_entries`, `cms_drafts`, `cms_revisions` |
19
20
  | Which rows of those a principal may see | this package: none, unless they are an author |
20
- | Which rows of your content a visitor may see | your binding's `visible`, which `published(...)` makes |
21
- | Who is an author, and who may make a transition | your `isAuthor` and `allow` |
21
+ | Which rows of your content a visitor may see | your content Entity's Read rules, compiled into its binding's `visible` |
22
+ | Who is an author, and who may make a transition | your guard set's principal guards, as `authors` and `transitions`; `allow` for a rule about the entry |
22
23
  | What publishing a post *does* | your own `create` and `update` handlers |
23
24
  | That a publish is whole | this package, inside the `transaction` you name |
24
25
  | The time | your `now`, which defaults to the server's clock |
@@ -39,28 +40,40 @@ a visitor's principal ──> published content rows, and nothing else
39
40
  ## Install
40
41
 
41
42
  ```sh
42
- pnpm add effect drizzle-orm foldkit-cms foldkit-cms-drizzle foldkit-remote-drizzle foldkit-remote-server
43
+ pnpm add effect drizzle-orm foldkit-cms foldkit-cms-drizzle foldkit-guard foldkit-remote-drizzle foldkit-remote-server
43
44
  ```
44
45
 
45
46
  ## Example
46
47
 
47
- The following is **wiring pseudocode**: `Blog`, `Posts`, `posts`, `isAuthor`,
48
- and the insert/update handlers belong to your application. Use
48
+ The following is **wiring pseudocode**: `Blog`, `Posts`, `posts`, `User`,
49
+ and the insert/update handlers belong to your application (a compiled version
50
+ is [`test/readme.test-d.ts`](test/readme.test-d.ts)). Use
49
51
  [the runnable server](../../examples/cms/src/server.ts) for complete handlers,
50
52
  a database layer, and transaction setup. `sqliteTables()` describes tables;
51
53
  include them in your migrations before running the server.
52
54
 
53
55
  ```text
54
- import { CmsServer, Transaction, published, sqliteTables } from 'foldkit-cms-drizzle'
56
+ import { CmsServer, Transaction, sqliteTables } from 'foldkit-cms-drizzle'
57
+ import { Guards } from 'foldkit-guard'
58
+ import { GuardDrizzle } from 'foldkit-guard/drizzle'
55
59
 
56
60
  const cmsTables = sqliteTables() // or pgTables(); add them to your schema and migrations
57
61
 
58
- const Db = bind(Blog, {
59
- Post: {
60
- table: posts,
61
- // A visitor sees what is published; an author sees every row.
62
- visible: published(posts.publishedAt, isAuthor),
63
- },
62
+ // Who is an author, who may publish, and what a visitor sees: a guard set.
63
+ const G = Guards.declare('Cms', User, guard => ({
64
+ isAuthor: guard.principal(),
65
+ isEditor: guard.principal(),
66
+ isPublished: guard.row(Blog.Post, () => Expr.isNotNull(Blog.Post.fields.publishedAt)),
67
+ }), { key: user => user.id })
68
+ const Policy = G.implement({
69
+ isAuthor: principal => Option.exists(principal, user => user.role !== 'reader'),
70
+ isEditor: principal => Option.exists(principal, user => user.role === 'editor'),
71
+ })
72
+ // A visitor sees what is published; an author sees every row.
73
+ const Post = Blog.Post.pipe(Guards.attach(G, { row: { Read: [G.isPublished, G.isAuthor] } }))
74
+
75
+ const Db = bind({ ...Blog, Post }, {
76
+ Post: { table: posts, visible: GuardDrizzle.visible(Policy, [Post]).Post },
64
77
  })
65
78
 
66
79
  // Your own handlers of the two mutations the content type publishes through.
@@ -69,9 +82,12 @@ const UpdatePost = RemoteServer.mutation(Posts.publish.update, ({ input }) => /*
69
82
 
70
83
  const cms = CmsServer.make({
71
84
  tables: cmsTables,
72
- content: [{ type: Posts, binding: Db.Post, create: CreatePost, update: UpdatePost }],
85
+ content: [{ type: Posts, entity: Post, binding: Db.Post, create: CreatePost, update: UpdatePost }],
73
86
  transaction: Transaction.statements, // one connection; Transaction.drizzle for Postgres
74
- isAuthor: principal => principal?.role === 'author',
87
+ policy: Policy,
88
+ authors: [G.isAuthor],
89
+ // An author writes; only an editor puts it in front of visitors.
90
+ transitions: { publish: [G.isEditor], schedule: [G.isEditor], unpublish: [G.isEditor] },
75
91
  })
76
92
 
77
93
  RemoteServer.make({
@@ -83,11 +99,20 @@ RemoteServer.make({
83
99
 
84
100
  - **Register `cms.sources`, not `source(Db.Post)`.** They are the content types'
85
101
  sources and the CMS's own, each behind the boundary.
86
- - **A content type that can be unpublished must be bound with a `visible` rule.**
87
- `CmsServer.make` throws for a type with a `published` role whose binding shows
88
- every row to everyone. It checks that a rule is there; whether the rule is
89
- right is yours to say, since only you know what a principal is.
102
+ - **A content type that can be unpublished must be guarded by whether it is.**
103
+ `CmsServer.make` throws for a type with a `published` role whose Read rule
104
+ does not read that field, and, through `GuardServer.validate`, for a binding
105
+ whose `visible` is not the rule your guard set built for it. `entity` is the
106
+ type's Entity with its guards attached, the one its table was bound from.
90
107
  - A content type with no `published` role has nothing to hide, and needs none.
108
+ - **Who is an author** is any of `authors`, principal guards; anyone else is a
109
+ visitor. Their bodies are asked with the interaction it is for, `Read` for
110
+ reading unpublished work and `Update` for any operation on it, so a reviewer
111
+ can read what they may not change. **Who may make a transition** is an author any of its
112
+ `transitions` guards lets; a transition not named is any author's. Archiving
113
+ an entry on show takes it off, so it asks `unpublish`'s guards too. `allow`
114
+ stays for a rule about the entry itself. A refusal is `Forbidden`, with its
115
+ sentence.
91
116
  - **`create` and `update` go to the CMS, not to `RemoteServer.make`.** Registered
92
117
  there too, they are a way to publish with no draft, no revision and no `allow`.
93
118
  - **Content that exists already is imported, not inserted.** `cms.import({ type,
@@ -132,7 +157,7 @@ new (`Cms.newEntryId()`), and the first save of an id nobody has makes the entry
132
157
 
133
158
  - **A save names what it was made from** (`basedOn`, the draft's `updatedAt`). A
134
159
  newer one on the server means someone else saved in between, and the save is
135
- refused with `CmsConflict: ...` instead of undoing their work. It is one
160
+ refused with `Cms.refusal.conflict` instead of undoing their work. It is one
136
161
  statement, compare and set, so two saves cannot both win.
137
162
  - A draft is bounded: `maxDraftSize` characters of JSON, values and Model
138
163
  together, one million by default. More is refused, and nothing is written.
@@ -142,8 +167,8 @@ new (`Cms.newEntryId()`), and the first save of an id nobody has makes the entry
142
167
  with no refetch. Every operation patches the entry with its `state` and its
143
168
  `revision`, so what is on screen follows what was done.
144
169
  - Refused: a type the server does not know, an entry that is not there or is of
145
- another type, an archived entry, a principal that is not an author, and an
146
- author your `allow` refuses.
170
+ another type, an archived entry, and, as `Forbidden`, a principal that is not
171
+ an author and an author your `transitions` or `allow` refuse.
147
172
 
148
173
  `CmsDiscardDraft` removes the working copy. A published entry is left as it was
149
174
  published. An entry that was never published has nothing left, so it is removed
@@ -171,7 +196,7 @@ fails after writing leaves nothing behind.
171
196
  done. A write of your own on the same connection does not take a turn unless
172
197
  you make it inside `Transaction.statements` too.
173
198
  - **A publish names the revision it was made from** (`basedOn`, the entry's
174
- `revision`, or `null`). A newer one is `CmsConflict: ...`, not a publish over
199
+ `revision`, or `null`). A newer one is refused with `Cms.refusal.conflict`, not a publish over
175
200
  someone else's. The entry's `revision` is compared and set first, inside the
176
201
  transaction, so of two publishes made from one revision one writes nothing.
177
202
  - A draft your mutation's Input refuses is not published, and the error says why.
@@ -187,9 +212,9 @@ A content type with a `slug` role is found by it: `cms.queries` has
187
212
  `Cms.bySlug(Posts)`, behind the same `visible` rule as every other read, so a
188
213
  visitor does not find what is not published.
189
214
 
190
- A publish to a slug another row has is refused as
191
- `CmsSlugTaken: slug: "hello" is already used`, and `Cms.slugTaken.key(message)`
192
- is the form key to show it on. **The check is advice; a unique index is the
215
+ A publish to a slug another row has is refused with
216
+ `Cms.refusal.field('slug', 'That address is taken: "hello" is already used')`,
217
+ which names the form key to show it on; a client reads it with `Data.refusal`. **The check is advice; a unique index is the
193
218
  rule.** Two publishes can both pass the check, and the one the index refuses
194
219
  gets the same error. Without a unique index on the column, that race publishes
195
220
  two rows at one address. The slug is read from the publish input's member of the
@@ -228,8 +253,8 @@ export default {
228
253
  draft changes: a fault the author must fix is not hammered every minute.
229
254
  - **What was promised is changed only by someone who could have promised it.**
230
255
  Since a scheduled draft is published as whoever scheduled it, saving over it,
231
- restoring over it or discarding it is refused to an author your `allow` would
232
- not let `schedule`. Otherwise someone who may not publish would publish through
256
+ restoring over it or discarding it is refused to an author your `transitions`
257
+ or `allow` would not let `schedule`. Otherwise someone who may not publish would publish through
233
258
  someone who may.
234
259
  - One failure does not stop the rest. `due` answers with each entry and its
235
260
  error, or `null`.
@@ -265,18 +290,19 @@ your `now`. An overdue scheduled publish reads overdue, with its reason.
265
290
  | Call | Meaning |
266
291
  | --- | --- |
267
292
  | `sqliteTables()`, `pgTables()` | The three tables, per dialect. `sqliteSchema` is their `create table` statements. |
268
- | `published(column, isAuthor)` | A `visible` rule for a content table: a visitor sees rows whose column is set. |
269
293
  | `Transaction.statements`, `Transaction.drizzle` | How a publish is made whole, by driver. |
270
- | `CmsServer.make({ tables, content, transaction, isAuthor, allow?, now?, nameOf?, maxDraftSize? })` | `sources`, `queries`, `mutations`, `due`, `import`, and `bindings`. |
294
+ | `CmsServer.make({ tables, content, transaction, policy, authors, transitions?, allow?, now?, nameOf?, maxDraftSize? })` | `sources`, `queries`, `mutations`, `due`, `import`, and `bindings`. Each `content` entry is `{ type, entity, binding, create, update }`. |
271
295
  | `cms.due(now, { as })` | Publishes what has come due; an Effect of `{ entry, error }` each. |
272
296
  | `cms.import({ type, values, as, at?, entry? })` | Content that is published already (a seed, or what another CMS held), written by the publish path; an Effect of `{ entry, targetId }`. |
273
297
 
274
298
  ## Limits
275
299
 
276
- - `allow` is asked about every transition, and again when an entry is read with
277
- `may`: the entry's `may` is the transitions `allow` lets the reader ask
278
- (`Cms.transitions` filtered), so the client can leave out what would be
279
- refused. Whether the entry offers one now is its `state`'s to say.
300
+ - `transitions` and `allow` are asked about every transition, and again when an
301
+ entry is read with `may`: the entry's `may` is the transitions they let the
302
+ reader ask (`Cms.transitions` filtered; each transition's guards decided once
303
+ per read), so the client can leave out what would be refused. Whether the
304
+ entry offers one now is its `state`'s to say. This `may` is the CMS's own,
305
+ not `foldkit-guard`'s.
280
306
  - `CmsPublish` needs a draft. To show an unpublished row again as it is, the
281
307
  editor's publish saves what is there first; a client of your own does the same.
282
308
  - A driver's refusal is recognised by its words (`unique` or `duplicate`, and the
package/dist/index.d.mts CHANGED
@@ -1,8 +1,10 @@
1
- import { AnyColumn } from "drizzle-orm";
2
- import { Effect, Schema } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { Content, Transition } from "foldkit-cms";
4
- import { AnyEntityBinding, DrizzleDatabase, Visible } from "foldkit-remote-drizzle";
5
- import { EntitySource, MutationSource, QuerySource, RemoteServerError } from "foldkit-remote-server";
3
+ import { AnyEntity } from "foldkit-entity";
4
+ import { Policy, PrincipalGuard } from "foldkit-guard";
5
+ import { AnyEntityBinding, DrizzleDatabase } from "foldkit-remote-drizzle";
6
+ import { EntitySource, MutationRefused, MutationSource, QuerySource, RemoteServerError } from "foldkit-remote-server";
7
+ import { RemoteForbidden } from "foldkit-remote";
6
8
  //#region src/tables.d.ts
7
9
  /**
8
10
  * - `cms_entries`: a piece of content from first keystroke to archive. It exists
@@ -806,21 +808,26 @@ type CmsTables = ReturnType<typeof sqliteTables> | ReturnType<typeof pgTables>;
806
808
  //#endregion
807
809
  //#region src/index.d.ts
808
810
  /**
809
- * A content type as the server holds it: its declaration, the binding of its
810
- * table, and the application's own handlers of its two publish mutations. What
811
+ * A content type as the server holds it: its declaration, its Entity with the
812
+ * guards attached that its binding was bound from, the binding of its table,
813
+ * and the application's own handlers of its two publish mutations. What
811
814
  * publishing a post does stays the application's code.
812
815
  */
813
816
  export interface ServedContent<P = any> {
814
817
  readonly type: Content<string, any, any, any>;
818
+ /** `type.entity` with the guard set's rules attached: `Post.pipe(Guards.attach(G, { … }))`. */
819
+ readonly entity: AnyEntity;
815
820
  readonly binding: AnyEntityBinding;
816
821
  readonly create: MutationSource<P, DrizzleDatabase>;
817
822
  readonly update: MutationSource<P, DrizzleDatabase>;
818
823
  }
819
824
  /**
820
825
  * Runs some work so that it happened entirely or did not. Drizzle's own
821
- * transactions differ by driver, so the application says which this is.
826
+ * transactions differ by driver, so the application says which this is. It
827
+ * fails as the work does, or, when the database refuses the transaction
828
+ * itself, as a `RemoteServerError` that says nothing of why.
822
829
  */
823
- export type Transaction = <A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) => Effect.Effect<A, E, DrizzleDatabase>;
830
+ export type Transaction = <A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) => Effect.Effect<A, E | RemoteServerError, DrizzleDatabase>;
824
831
  export declare const Transaction: {
825
832
  /**
826
833
  * `begin`, the work, then `commit` or `rollback`, as statements. For a database
@@ -836,18 +843,29 @@ export declare const Transaction: {
836
843
  */
837
844
  drizzle: Transaction;
838
845
  };
839
- export interface CmsServerConfig<P> {
846
+ export interface CmsServerConfig<User> {
840
847
  readonly tables: CmsTables;
841
- readonly content: ReadonlyArray<ServedContent<P>>;
848
+ readonly content: ReadonlyArray<ServedContent<Option.Option<User>>>;
842
849
  /** How a publish is made whole: `Transaction.statements` or `Transaction.drizzle`. */
843
850
  readonly transaction: Transaction;
844
- /** Who reads and writes unpublished work. Everyone else is a visitor. */
845
- readonly isAuthor: (principal: P) => boolean;
851
+ /** The guard set's bodies, which decide `authors` and `transitions`. */
852
+ readonly policy: Policy<User>;
846
853
  /**
847
- * Whether this author may make this transition. It is asked after the entry is
848
- * found and the transition is one its state offers. Default: any author may.
854
+ * Who reads and writes unpublished work: a principal any of these lets.
855
+ * Everyone else is a visitor.
849
856
  */
850
- readonly allow?: (principal: P, transition: Transition, entry: EntryRow) => boolean;
857
+ readonly authors: ReadonlyArray<PrincipalGuard<User>>;
858
+ /**
859
+ * Who of the authors may make a transition: any of its guards lets. One not
860
+ * named is any author's.
861
+ */
862
+ readonly transitions?: { readonly [T in Transition]?: ReadonlyArray<PrincipalGuard<User>> | undefined; } | undefined;
863
+ /**
864
+ * Whether this author may make this transition of this entry, for a rule
865
+ * about the entry itself: asked after the entry is found, the transition is
866
+ * one its state offers, and `transitions` let. Default: they may.
867
+ */
868
+ readonly allow?: (principal: Option.Option<User>, transition: Transition, entry: EntryRow) => boolean;
851
869
  /** The server's clock, passed in so a test can hold it. */
852
870
  readonly now?: () => Date;
853
871
  /**
@@ -857,7 +875,7 @@ export interface CmsServerConfig<P> {
857
875
  */
858
876
  readonly maxDraftSize?: number;
859
877
  /** Who a principal is, for `createdBy` and `updatedBy`. */
860
- readonly nameOf?: (principal: P) => string | null;
878
+ readonly nameOf?: (principal: Option.Option<User>) => string | null;
861
879
  }
862
880
  /** A row of `cms_entries`, as the server's rules see it. */
863
881
  export interface EntryRow {
@@ -883,33 +901,26 @@ export interface ImportItem<P> {
883
901
  readonly at?: Date | undefined;
884
902
  readonly entry?: string | undefined;
885
903
  }
886
- /**
887
- * For a content table's binding: a visitor sees the rows whose `published`
888
- * column is set, and an author sees every row. Give it as the storage's
889
- * `visible` when the table is bound; `CmsServer.make` refuses a content type
890
- * that can be unpublished and whose binding shows every row to everyone.
891
- */
892
- export declare const published: <P>(column: AnyColumn, isAuthor: (principal: P) => boolean) => Visible;
893
904
  export declare const CmsServer: {
894
- make: <P = unknown>(config: CmsServerConfig<P>) => {
905
+ make: <User>(config: CmsServerConfig<User>) => {
895
906
  /**
896
907
  * The sources of the content types and of the CMS's own Entities, each behind
897
908
  * the audience boundary. Register these and not `source(binding)` of a content
898
909
  * table: that is how the boundary cannot be forgotten.
899
910
  */
900
- sources: readonly EntitySource<P, DrizzleDatabase>[];
901
- queries: ReadonlyArray<QuerySource<P, DrizzleDatabase>>;
902
- mutations: readonly MutationSource<P, DrizzleDatabase>[];
911
+ sources: readonly EntitySource<Option.Option<User>, DrizzleDatabase>[];
912
+ queries: ReadonlyArray<QuerySource<Option.Option<User>, DrizzleDatabase>>;
913
+ mutations: readonly MutationSource<Option.Option<User>, DrizzleDatabase>[];
903
914
  due: (at: Date, options: {
904
- readonly as: (scheduledBy: string | null) => P;
915
+ readonly as: (scheduledBy: string | null) => Option.Option<User>;
905
916
  }) => Effect.Effect<{
906
917
  readonly entry: string;
907
918
  readonly error: string | null;
908
- }[], never, DrizzleDatabase>;
909
- import: (item: ImportItem<P>) => Effect.Effect<{
919
+ }[], RemoteServerError, DrizzleDatabase>;
920
+ import: (item: ImportItem<Option.Option<User>>) => Effect.Effect<{
910
921
  entry: string;
911
922
  targetId: string;
912
- }, RemoteServerError, DrizzleDatabase>;
923
+ }, RemoteServerError | MutationRefused | RemoteForbidden, DrizzleDatabase>;
913
924
  /** The bindings of `Entry`, `Draft` and `Revision`, for a handler that returns patches of them. */
914
925
  bindings: {
915
926
  readonly Entry: import("foldkit-remote-drizzle").EntityBinding<"CmsEntry", import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/tables.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;qBAea;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCA;KAgBD,YAAY,kBAAkB,gBAAgB,kBAAkB;;;;;;;;iBC1C3D,cAAc;WACpB,MAAM;WACN,SAAS;WACT,QAAQ,eAAe,GAAG;WAC1B,QAAQ,eAAe,GAAG;;;;;;YAOzB,eAAe,GAAG,GAC5B,MAAM,OAAO,OAAO,GAAG,GAAG,qBACvB,OAAO,OAAO,GAAG,GAAG;qBAyBZ;;;;;;;;EAiBF,YAAA;;;;;EA0BA,SAAA;;iBAGM,gBAAgB;WACtB,QAAQ;WACR,SAAS,cAAc,cAAc;;WAErC,aAAa;;WAEb,WAAW,WAAW;;;;;WAKtB,SAAS,WAAW,GAAG,YAAY,YAAY,OAAO;;WAEtD,YAAY;;;;;;WAMZ;;WAEA,UAAU,WAAW;;;iBAIf;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;iBAWM,WAAW;WACjB;WACA;WACA,IAAI;WACJ,KAAK;WACL;;;;;;;;qBAmCE,YACV,GAAG,QAAQ,WAAW,WAAW,WAAW,kBAAgB;qBAIlD;EACJ,OAAA,aAAC,QAAoB,gBAAgB;;;;;;;IAq6BN,SAAA,cAAc,YAAY,GAAG;;IAvKhD,MAAA,IAAA,MAAI;eAAsB,KAAK,+BAA+B;UAAG,OAAA;eAmBtC;eAAwB;;IAkC3C,SAAA,MAAA,WAAW,OAAE,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/tables.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;qBAea;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCA;KAgBD,YAAY,kBAAkB,gBAAgB,kBAAkB;;;;;;;;;iBCnC3D,cAAc;WACpB,MAAM;;WAEN,QAAQ;WACR,SAAS;WACT,QAAQ,eAAe,GAAG;WAC1B,QAAQ,eAAe,GAAG;;;;;;;;YASzB,eAAe,GAAG,GAC5B,MAAM,OAAO,OAAO,GAAG,GAAG,qBACvB,OAAO,OAAO,GAAG,IAAI,mBAAmB;qBAyBhC;;;;;;;;EAiBF,YAAA;;;;;EA4BA,SAAA;;iBAGM,gBAAgB;WACtB,QAAQ;WACR,SAAS,cAAc,cAAc,OAAO,OAAO;;WAEnD,aAAa;;WAEb,QAAQ,OAAO;;;;;WAKf,SAAS,cAAc,eAAe;;;;;WAKtC,0BACK,KAAK,cAAc,cAAc,eAAe;;;;;;WAMrD,SACP,WAAW,OAAO,OAAO,OACzB,YAAY,YACZ,OAAO;;WAGA,YAAY;;;;;;WAMZ;;WAEA,UAAU,WAAW,OAAO,OAAO;;;iBAI7B;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;iBAWM,WAAW;WACjB;WACA;WACA,IAAI;WACJ,KAAK;WACL;;qBA6BE;EACJ,OAAA,MAAI,QAAU,gBAAgB;;;;;;;IAuhCC,SAAA,cAAc,YAAW,OAAA,OAAA,OAAI;;IAxLhD,MAAA,IAAA,MAAI;eAAsB,KAAK,+BAA0B,OAAA,OAAA;UAAQ,OAAA;eAmBtC;eAAwB;;IAkC3C,SAAA,MAAA,WAAU,OAAA,OAAA,WAAG,OAAA"}
package/dist/index.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import { and, eq, inArray, isNotNull, isNull, lte, ne, sql } from "drizzle-orm";
2
- import { Effect, Exit, Schema, Semaphore } from "effect";
2
+ import { Effect, Exit, Option, Schema, Semaphore } from "effect";
3
3
  import { Cms } from "foldkit-cms";
4
- import { DrizzleDatabase, bind, drizzleWrites, query, returning, source } from "foldkit-remote-drizzle";
4
+ import { Entity } from "foldkit-entity";
5
+ import { Guards } from "foldkit-guard";
6
+ import { GuardServer } from "foldkit-guard/server";
7
+ import { DrizzleDatabase, bind, drizzleWrites, query, returning, runStatement, source } from "foldkit-remote-drizzle";
5
8
  import { RemoteServer, RemoteServerError } from "foldkit-remote-server";
6
9
  import { integer, jsonb, pgTable, text } from "drizzle-orm/pg-core";
7
10
  import { integer as integer$1, sqliteTable, text as text$1 } from "drizzle-orm/sqlite-core";
@@ -104,10 +107,11 @@ create table if not exists cms_revisions (
104
107
  * `foldkit-cms-drizzle` — the server half of `foldkit-cms`.
105
108
  *
106
109
  * It keeps an author's unpublished work in three tables beside the
107
- * application's own, and it is where the audience boundary is enforced: a
108
- * principal that is not an author is refused entries, drafts and revisions
109
- * outright, and sees of the content only the rows its `published` role shows.
110
- * Both are a binding's `visible`, so they hold on every path a table is read by.
110
+ * application's own, and it is where the audience boundary is enforced, by a
111
+ * `foldkit-guard` guard set: a principal its `authors` guards refuse is refused
112
+ * entries, drafts and revisions outright, and sees of the content only the
113
+ * rows the content's own Read rules show. Both are a binding's `visible`, so
114
+ * they hold on every path a table is read by.
111
115
  *
112
116
  * Publishing runs the application's own mutation with the draft's value, inside
113
117
  * a transaction that also appends the revision and removes the draft, so a
@@ -145,12 +149,12 @@ const Transaction = {
145
149
  const database = yield* DrizzleDatabase;
146
150
  const failed = Symbol("failed");
147
151
  let exit;
148
- yield* Effect.promise(() => database.transaction(async (inner) => {
152
+ yield* runStatement(() => database.transaction(async (inner) => {
149
153
  exit = await Effect.runPromiseExit(work.pipe(Effect.provideService(DrizzleDatabase, inner)));
150
154
  if (!Exit.isSuccess(exit)) throw failed;
151
155
  }).catch((error) => {
152
156
  if (error !== failed) throw error;
153
- }));
157
+ }), "Database write failed");
154
158
  return yield* exit;
155
159
  }))
156
160
  };
@@ -172,25 +176,35 @@ const refusedAsDuplicate = (defect, column) => {
172
176
  const named = new RegExp(`(^|[^a-zA-Z0-9])${column.replace(/[^\w]/g, ".")}([^a-zA-Z0-9]|$)`);
173
177
  return /unique|duplicate/i.test(text) && named.test(text);
174
178
  };
175
- /**
176
- * For a content table's binding: a visitor sees the rows whose `published`
177
- * column is set, and an author sees every row. Give it as the storage's
178
- * `visible` when the table is bound; `CmsServer.make` refuses a content type
179
- * that can be unpublished and whose binding shows every row to everyone.
180
- */
181
- const published = (column, isAuthor) => (principal) => isAuthor(principal) ? void 0 : isNotNull(column);
182
179
  const CmsServer = { make: (config) => {
183
- const { tables, isAuthor } = config;
180
+ const { tables, policy } = config;
184
181
  const now = config.now ?? (() => /* @__PURE__ */ new Date());
185
182
  const nameOf = config.nameOf ?? (() => null);
186
183
  const maxDraftSize = config.maxDraftSize ?? 1e6;
187
184
  const byType = new Map(config.content.map((served) => [served.type.name, served]));
188
- for (const { type, binding, create, update } of config.content) {
185
+ for (const { type, entity, create, update } of config.content) {
186
+ if (!Entity.same(entity, type.entity)) throw new Error(`foldkit-cms-drizzle: content "${type.name}" is of ${type.entity.name}, but was given ${entity.name}'s guards`);
189
187
  for (const [handler, declared] of [[create, type.publish.create], [update, type.publish.update]]) if (handler.mutation !== declared.name) throw new Error(`foldkit-cms-drizzle: content "${type.name}" publishes through "${declared.name}", but was given the handler of "${handler.mutation}"`);
190
- if (type.roles.published === void 0) continue;
191
- if (binding.visible === void 0) throw new Error(`foldkit-cms-drizzle: content "${type.name}" can be unpublished, but its binding shows every row to everyone; bind its table with \`visible: published(column, isAuthor)\``);
188
+ const shown = type.roles.published;
189
+ if (shown === void 0) continue;
190
+ if (!Guards.guardFields(entity, "Read").includes(shown.key)) throw new Error(`foldkit-cms-drizzle: content "${type.name}" can be unpublished, but its Read rule does not read "${shown.key}"; attach one that does, such as \`Read: [G.isPublished, G.isAuthor]\``);
192
191
  }
193
- const authorsOnly = (principal) => isAuthor(principal) ? void 0 : sql`0 = 1`;
192
+ /** Whether any of `guards` lets `principal`, each body told `interaction`. */
193
+ const anyOf = (guards, principal, interaction) => Effect.map(Effect.forEach(guards, (guard) => policy.decide(guard, {
194
+ principal,
195
+ interaction
196
+ })), (decisions) => decisions.some((decision) => decision === true));
197
+ /**
198
+ * Whether `principal` is an author, asked as the interaction it is for:
199
+ * reading unpublished work, or changing it, so a body can tell the two.
200
+ */
201
+ const isAuthor = (principal, interaction) => anyOf(config.authors, principal, interaction);
202
+ /** Whether `transitions` let this author make `transition`; an author's when it names none. */
203
+ const transitionLets = (principal, transition) => Option.match(Option.fromUndefinedOr(config.transitions?.[transition]), {
204
+ onNone: () => Effect.succeed(true),
205
+ onSome: (guards) => anyOf(guards, principal, transition)
206
+ });
207
+ const authorsOnly = (principal) => policy.isPrincipal(principal) ? Effect.map(isAuthor(principal, "Read"), (author) => author ? void 0 : sql`0 = 1`) : Effect.die(/* @__PURE__ */ new Error("[foldkit-cms-drizzle] the principal is not an Option of the guard set’s"));
194
208
  const Db = bind(Cms.Entities, {
195
209
  Entry: {
196
210
  table: tables.entries,
@@ -223,11 +237,11 @@ const CmsServer = { make: (config) => {
223
237
  const factsOf = (entries) => Effect.gen(function* () {
224
238
  const database = yield* DrizzleDatabase;
225
239
  const ids = entries.map((entry) => entry.id);
226
- const drafts = ids.length === 0 ? [] : yield* Effect.promise(() => Promise.resolve(database.select({
240
+ const drafts = ids.length === 0 ? [] : yield* runStatement(() => database.select({
227
241
  id: tables.drafts.id,
228
242
  scheduledFor: tables.drafts.scheduledFor,
229
243
  scheduleError: tables.drafts.scheduleError
230
- }).from(tables.drafts).where(inArray(tables.drafts.id, ids))));
244
+ }).from(tables.drafts).where(inArray(tables.drafts.id, ids)), "Database query failed");
231
245
  const draftOf = new Map(drafts.map((draft) => [String(draft.id), draft]));
232
246
  const rows = /* @__PURE__ */ new Map();
233
247
  for (const [name, { type, binding }] of byType) {
@@ -235,10 +249,10 @@ const CmsServer = { make: (config) => {
235
249
  if (targets.length === 0) continue;
236
250
  const id = binding.columns.id;
237
251
  const publishedColumn = type.roles.published === void 0 ? void 0 : binding.columns[type.roles.published.key];
238
- const found = yield* Effect.promise(() => Promise.resolve(database.select({
252
+ const found = yield* runStatement(() => database.select({
239
253
  id,
240
254
  ...publishedColumn === void 0 ? {} : { shown: publishedColumn }
241
- }).from(binding.table).where(inArray(id, targets))));
255
+ }).from(binding.table).where(inArray(id, targets)), "Database query failed");
242
256
  for (const row of found) rows.set(`${name}:${String(row.id)}`, publishedColumn === void 0 || row.shown !== null ? "visible" : "hidden");
243
257
  }
244
258
  return new Map(entries.map((entry) => {
@@ -263,7 +277,7 @@ const CmsServer = { make: (config) => {
263
277
  };
264
278
  const findEntry = (id) => Effect.gen(function* () {
265
279
  const database = yield* DrizzleDatabase;
266
- const [row] = yield* Effect.promise(() => Promise.resolve(database.select(entryColumns).from(tables.entries).where(eq(tables.entries.id, id)).limit(1)));
280
+ const [row] = yield* runStatement(() => database.select(entryColumns).from(tables.entries).where(eq(tables.entries.id, id)).limit(1), "Database query failed");
267
281
  return row;
268
282
  });
269
283
  const generated = source(Db.Entry);
@@ -294,6 +308,7 @@ const CmsServer = { make: (config) => {
294
308
  }));
295
309
  const facts = yield* factsOf(rows);
296
310
  const at = now();
311
+ const lets = new Map(wantsMay ? yield* Effect.forEach(Cms.transitions, (transition) => Effect.map(transitionLets(context.principal, transition), (allowed) => [transition, allowed])) : []);
297
312
  return records.map((record, index) => {
298
313
  const row = rows[index];
299
314
  const known = facts.get(record.id);
@@ -302,7 +317,7 @@ const CmsServer = { make: (config) => {
302
317
  values: {
303
318
  ...Object.fromEntries(Object.entries(record.values).filter(([field]) => field === "id" || context.fields.includes(field))),
304
319
  ...wantsState ? { state: Cms.state(known, at) } : {},
305
- ...wantsMay ? { may: Cms.transitions.filter((transition) => config.allow?.(context.principal, transition, row) !== false) } : {}
320
+ ...wantsMay ? { may: Cms.transitions.filter((transition) => lets.get(transition) === true && config.allow?.(context.principal, transition, row) !== false) } : {}
306
321
  }
307
322
  };
308
323
  });
@@ -310,7 +325,7 @@ const CmsServer = { make: (config) => {
310
325
  };
311
326
  const worklist = query(Cms.Entries, { entity: Db.Entry });
312
327
  /** A mutation of this server: its principal and its database are fixed, its input is the descriptor's. */
313
- const operation = (descriptor, run) => RemoteServer.mutation(descriptor, (context) => isAuthor(context.principal) ? config.transaction(run(context)) : Effect.fail(refuse("Only an author may change unpublished work")));
328
+ const operation = (descriptor, run) => RemoteServer.mutation(descriptor, (context) => Effect.flatMap(isAuthor(context.principal, "Update"), (author) => author ? config.transaction(run(context)) : Effect.fail(RemoteServer.forbid("Only an author may change unpublished work"))));
314
329
  /**
315
330
  * A scheduled draft is published, when its time comes, as whoever scheduled it.
316
331
  * So changing it, replacing it or removing it is for someone who could have
@@ -320,10 +335,13 @@ const CmsServer = { make: (config) => {
320
335
  const promised = (principal, entry) => Effect.gen(function* () {
321
336
  const [draft] = yield* readRows(Db.Draft, { scheduledFor: tables.drafts.scheduledFor }, entry.id);
322
337
  if (draft?.scheduledFor == null) return;
323
- if (config.allow?.(principal, "schedule", entry) === false) return yield* refuse("This draft is scheduled, and only someone who may schedule it may change it");
338
+ if (!(yield* transitionLets(principal, "schedule")) || config.allow?.(principal, "schedule", entry) === false) return yield* RemoteServer.forbid("This draft is scheduled, and only someone who may schedule it may change it");
324
339
  });
325
- /** Whether the application lets this author do this, once the entry is known. */
326
- const asking = (principal, transition, entry) => entry !== void 0 && config.allow?.(principal, transition, entry) === false ? Effect.fail(refuse(`This author may not ${transition} this entry`)) : Effect.void;
340
+ /**
341
+ * Whether this author may make this transition: its `transitions`, and `allow`
342
+ * of the entry once there is one.
343
+ */
344
+ const asking = (principal, transition, entry) => Effect.flatMap(transitionLets(principal, transition), (lets) => !lets || entry !== void 0 && config.allow?.(principal, transition, entry) === false ? Effect.fail(RemoteServer.forbid(`This author may not ${transition} this entry`)) : Effect.void);
327
345
  const draftPatch = returning(Db.Draft, [
328
346
  "values",
329
347
  "model",
@@ -359,7 +377,7 @@ const CmsServer = { make: (config) => {
359
377
  });
360
378
  const readRows = (binding, columns, id) => Effect.gen(function* () {
361
379
  const database = yield* DrizzleDatabase;
362
- return yield* Effect.promise(() => Promise.resolve(database.select(columns).from(binding.table).where(eq(binding.columns.id, id))));
380
+ return yield* runStatement(() => database.select(columns).from(binding.table).where(eq(binding.columns.id, id)), "Database query failed");
363
381
  });
364
382
  const SaveDraft = operation(Cms.Operations.SaveDraft, ({ input, principal }) => Effect.gen(function* () {
365
383
  if (!byType.has(input.type)) return yield* refuse(`"${input.type}" is not a type of content this server knows`);
@@ -385,8 +403,8 @@ const CmsServer = { make: (config) => {
385
403
  updatedBy: who,
386
404
  scheduleError: null
387
405
  };
388
- if (previous === null && input.basedOn !== null) return yield* refuse("CmsConflict: this draft was discarded");
389
- if (existing === void 0) yield* Effect.promise(() => Promise.resolve(database.insert(tables.entries).values({
406
+ if (previous === null && input.basedOn !== null) return yield* RemoteServer.refuse(Cms.Operations.SaveDraft, Cms.refusal.conflict, "This draft was discarded");
407
+ if (existing === void 0) yield* runStatement(() => database.insert(tables.entries).values({
390
408
  id,
391
409
  type: input.type,
392
410
  targetId: null,
@@ -395,14 +413,14 @@ const CmsServer = { make: (config) => {
395
413
  createdAt: at,
396
414
  archivedAt: null,
397
415
  revision: null
398
- })));
399
- if (previous === null) yield* Effect.promise(() => Promise.resolve(database.insert(tables.drafts).values({
416
+ }), "Database write failed");
417
+ if (previous === null) yield* runStatement(() => database.insert(tables.drafts).values({
400
418
  id,
401
419
  ...written,
402
420
  baseRevision: null
403
- })));
404
- else if ((yield* Effect.promise(() => Promise.resolve(database.update(tables.drafts).set(written).where(and(eq(tables.drafts.id, id), eq(tables.drafts.updatedAt, input.basedOn ?? ""))).returning({ id: tables.drafts.id })))).length === 0) return yield* refuse("CmsConflict: this draft was saved by someone else since");
405
- if (existing !== void 0 && existing.label !== input.label) yield* Effect.promise(() => Promise.resolve(database.update(tables.entries).set({ label: input.label }).where(eq(tables.entries.id, id)).returning({ id: tables.entries.id })));
421
+ }), "Database write failed");
422
+ else if ((yield* runStatement(() => database.update(tables.drafts).set(written).where(and(eq(tables.drafts.id, id), eq(tables.drafts.updatedAt, input.basedOn ?? ""))).returning({ id: tables.drafts.id }), "Database write failed")).length === 0) return yield* RemoteServer.refuse(Cms.Operations.SaveDraft, Cms.refusal.conflict, "This draft was saved by someone else since");
423
+ if (existing !== void 0 && existing.label !== input.label) yield* runStatement(() => database.update(tables.entries).set({ label: input.label }).where(eq(tables.entries.id, id)).returning({ id: tables.entries.id }), "Database write failed");
406
424
  const entities = [...yield* entryPatches(id), ...draftPatch.patches(yield* readRows(Db.Draft, draftPatch.columns, id))];
407
425
  return {
408
426
  output: {
@@ -418,7 +436,7 @@ const CmsServer = { make: (config) => {
418
436
  yield* asking(principal, "discard", entry);
419
437
  yield* promised(principal, entry);
420
438
  const database = yield* drizzleWrites;
421
- yield* Effect.promise(() => Promise.resolve(database.delete(tables.drafts).where(eq(tables.drafts.id, entry.id))));
439
+ yield* runStatement(() => database.delete(tables.drafts).where(eq(tables.drafts.id, entry.id)), "Database write failed");
422
440
  const gone = [{
423
441
  entity: "CmsDraft",
424
442
  id: entry.id
@@ -428,7 +446,7 @@ const CmsServer = { make: (config) => {
428
446
  deleted: gone,
429
447
  entities: yield* entryPatches(entry.id)
430
448
  };
431
- yield* Effect.promise(() => Promise.resolve(database.delete(tables.entries).where(eq(tables.entries.id, entry.id))));
449
+ yield* runStatement(() => database.delete(tables.entries).where(eq(tables.entries.id, entry.id)), "Database write failed");
432
450
  return {
433
451
  output: {},
434
452
  deleted: [...gone, {
@@ -493,42 +511,43 @@ const CmsServer = { make: (config) => {
493
511
  const { entry, served } = found;
494
512
  const database = yield* DrizzleDatabase;
495
513
  const held = entry.revision;
496
- const conflict = refuse("CmsConflict: this entry was published by someone else since");
514
+ const conflict = RemoteServer.refuse(Cms.Operations.Publish, Cms.refusal.conflict, "This entry was published by someone else since");
497
515
  if (held !== basedOn) return yield* conflict;
498
516
  const { draft, creating, handler, decoded } = yield* readied(found);
499
517
  const address = served.type.roles.slug;
500
518
  const slug = address === void 0 ? void 0 : decoded?.[address.key];
501
- const taken = (key) => refuse(Cms.slugTaken.message(key, String(slug)));
519
+ const taken = (key) => RemoteServer.refuse(Cms.Operations.Publish, Cms.refusal.field(key, `That address is taken: "${String(slug)}" is already used`));
502
520
  if (address !== void 0 && typeof slug === "string") {
503
521
  const column = served.binding.columns[address.key];
504
522
  const id = served.binding.columns.id;
505
- if ((yield* Effect.promise(() => Promise.resolve(database.select({ id }).from(served.binding.table).where(and(eq(column, slug), creating ? void 0 : ne(id, entry.targetId))).limit(1)))).length > 0) return yield* taken(address.key);
523
+ if ((yield* runStatement(() => database.select({ id }).from(served.binding.table).where(and(eq(column, slug), creating ? void 0 : ne(id, entry.targetId))).limit(1), "Database query failed")).length > 0) return yield* taken(address.key);
506
524
  }
507
525
  return yield* Effect.gen(function* () {
508
526
  const writes = yield* drizzleWrites;
509
527
  const n = (held ?? 0) + 1;
510
- if ((yield* Effect.promise(() => Promise.resolve(writes.update(tables.entries).set({ revision: n }).where(and(eq(tables.entries.id, entry.id), held === null ? isNull(tables.entries.revision) : eq(tables.entries.revision, held))).returning({ id: tables.entries.id })))).length === 0) return yield* conflict;
528
+ if ((yield* runStatement(() => writes.update(tables.entries).set({ revision: n }).where(and(eq(tables.entries.id, entry.id), held === null ? isNull(tables.entries.revision) : eq(tables.entries.revision, held))).returning({ id: tables.entries.id }), "Database write failed")).length === 0) return yield* conflict;
511
529
  const ran = yield* handler.run({
512
530
  input: decoded,
513
- principal
531
+ principal,
532
+ requestId: `publish:${entry.id}:${held ?? "new"}`
514
533
  });
515
534
  const targetId = creating ? String(ran.output.id) : entry.targetId;
516
535
  const shown = served.type.roles.published;
517
536
  if (shown !== void 0) {
518
537
  const column = served.binding.columns[shown.key];
519
- yield* Effect.promise(() => Promise.resolve(writes.update(served.binding.table).set({ [shown.key]: stamp(column, at) }).where(and(eq(served.binding.columns.id, targetId), isNull(column))).returning({ id: served.binding.columns.id })));
538
+ yield* runStatement(() => writes.update(served.binding.table).set({ [shown.key]: stamp(column, at) }).where(and(eq(served.binding.columns.id, targetId), isNull(column))).returning({ id: served.binding.columns.id }), "Database write failed");
520
539
  }
521
540
  const revisionId = `${entry.id}:${n}`;
522
- yield* Effect.promise(() => Promise.resolve(writes.insert(tables.revisions).values({
541
+ yield* runStatement(() => writes.insert(tables.revisions).values({
523
542
  id: revisionId,
524
543
  entryId: entry.id,
525
544
  n,
526
545
  values: draft?.values,
527
546
  publishedAt: at.toISOString(),
528
547
  publishedBy: nameOf(principal)
529
- })));
530
- yield* Effect.promise(() => Promise.resolve(writes.delete(tables.drafts).where(eq(tables.drafts.id, entry.id))));
531
- yield* Effect.promise(() => Promise.resolve(writes.update(tables.entries).set({ targetId }).where(eq(tables.entries.id, entry.id)).returning({ id: tables.entries.id })));
548
+ }), "Database write failed");
549
+ yield* runStatement(() => writes.delete(tables.drafts).where(eq(tables.drafts.id, entry.id)), "Database write failed");
550
+ yield* runStatement(() => writes.update(tables.entries).set({ targetId }).where(eq(tables.entries.id, entry.id)).returning({ id: tables.entries.id }), "Database write failed");
532
551
  return {
533
552
  output: {
534
553
  entry: entry.id,
@@ -557,7 +576,7 @@ const CmsServer = { make: (config) => {
557
576
  /** Writes to an entry's draft, and answers with both as the client should now hold them. */
558
577
  const drafted = (id, values) => Effect.gen(function* () {
559
578
  const writes = yield* drizzleWrites;
560
- yield* Effect.promise(() => Promise.resolve(writes.update(tables.drafts).set(values).where(eq(tables.drafts.id, id)).returning({ id: tables.drafts.id })));
579
+ yield* runStatement(() => writes.update(tables.drafts).set(values).where(eq(tables.drafts.id, id)).returning({ id: tables.drafts.id }), "Database write failed");
561
580
  return {
562
581
  output: {},
563
582
  entities: [...yield* entryPatches(id), ...scheduledPatch.patches(yield* readRows(Db.Draft, scheduledPatch.columns, id))]
@@ -587,14 +606,19 @@ const CmsServer = { make: (config) => {
587
606
  const archiving = (transition) => operation(transition === "archive" ? Cms.Operations.Archive : Cms.Operations.Unarchive, ({ input, principal }) => Effect.gen(function* () {
588
607
  const { entry, served } = yield* offering(input.entry, transition);
589
608
  yield* asking(principal, transition, entry);
590
- const writes = yield* drizzleWrites;
591
- yield* Effect.promise(() => Promise.resolve(writes.update(tables.entries).set({ archivedAt: transition === "archive" ? now().toISOString() : null }).where(eq(tables.entries.id, entry.id)).returning({ id: tables.entries.id })));
592
609
  const shown = served.type.roles.published;
610
+ if (transition === "archive" && shown !== void 0 && entry.targetId !== null) {
611
+ const published = returning(served.binding, [shown.key]);
612
+ const [row] = published.patches(yield* readRows(served.binding, published.columns, entry.targetId));
613
+ if (row !== void 0 && Option.isSome(Option.fromNullishOr(row.values[shown.key]))) yield* asking(principal, "unpublish", entry);
614
+ }
615
+ const writes = yield* drizzleWrites;
616
+ yield* runStatement(() => writes.update(tables.entries).set({ archivedAt: transition === "archive" ? now().toISOString() : null }).where(eq(tables.entries.id, entry.id)).returning({ id: tables.entries.id }), "Database write failed");
593
617
  if (transition === "unarchive" || shown === void 0 || entry.targetId === null) return {
594
618
  output: {},
595
619
  entities: yield* entryPatches(entry.id)
596
620
  };
597
- yield* Effect.promise(() => Promise.resolve(writes.update(served.binding.table).set({ [shown.key]: null }).where(eq(served.binding.columns.id, entry.targetId)).returning({ id: served.binding.columns.id })));
621
+ yield* runStatement(() => writes.update(served.binding.table).set({ [shown.key]: null }).where(eq(served.binding.columns.id, entry.targetId)).returning({ id: served.binding.columns.id }), "Database write failed");
598
622
  const content = returning(served.binding, [shown.key]);
599
623
  return {
600
624
  output: {},
@@ -612,7 +636,7 @@ const CmsServer = { make: (config) => {
612
636
  yield* asking(principal, "restore", entry);
613
637
  yield* promised(principal, entry);
614
638
  const database = yield* DrizzleDatabase;
615
- const [revision] = yield* Effect.promise(() => Promise.resolve(database.select({ values: tables.revisions.values }).from(tables.revisions).where(and(eq(tables.revisions.entryId, entry.id), eq(tables.revisions.n, input.revision))).limit(1)));
639
+ const [revision] = yield* runStatement(() => database.select({ values: tables.revisions.values }).from(tables.revisions).where(and(eq(tables.revisions.entryId, entry.id), eq(tables.revisions.n, input.revision))).limit(1), "Database query failed");
616
640
  if (revision === void 0) return yield* refuse(`This entry has no revision ${input.revision}`);
617
641
  const writes = yield* drizzleWrites;
618
642
  const [held] = yield* readRows(Db.Draft, { updatedAt: tables.drafts.updatedAt }, entry.id);
@@ -629,10 +653,10 @@ const CmsServer = { make: (config) => {
629
653
  scheduledBy: null,
630
654
  scheduleError: null
631
655
  };
632
- yield* Effect.promise(() => Promise.resolve(held === void 0 ? writes.insert(tables.drafts).values({
656
+ yield* runStatement(() => held === void 0 ? writes.insert(tables.drafts).values({
633
657
  id: entry.id,
634
658
  ...restored
635
- }) : writes.update(tables.drafts).set(restored).where(eq(tables.drafts.id, entry.id)).returning({ id: tables.drafts.id })));
659
+ }) : writes.update(tables.drafts).set(restored).where(eq(tables.drafts.id, entry.id)).returning({ id: tables.drafts.id }), "Database write failed");
636
660
  return {
637
661
  output: { updatedAt },
638
662
  entities: [
@@ -651,16 +675,16 @@ const CmsServer = { make: (config) => {
651
675
  */
652
676
  const due = (at, options) => Effect.gen(function* () {
653
677
  const database = yield* DrizzleDatabase;
654
- const waiting = yield* Effect.promise(() => Promise.resolve(database.select({
678
+ const waiting = yield* runStatement(() => database.select({
655
679
  id: tables.drafts.id,
656
680
  scheduledBy: tables.drafts.scheduledBy
657
- }).from(tables.drafts).where(and(isNotNull(tables.drafts.scheduledFor), lte(tables.drafts.scheduledFor, at.toISOString()), isNull(tables.drafts.scheduleError), sql`${tables.drafts.id} in (select ${tables.entries.id} from ${tables.entries} where ${tables.entries.archivedAt} is null)`))));
681
+ }).from(tables.drafts).where(and(isNotNull(tables.drafts.scheduledFor), lte(tables.drafts.scheduledFor, at.toISOString()), isNull(tables.drafts.scheduleError), sql`${tables.drafts.id} in (select ${tables.entries.id} from ${tables.entries} where ${tables.entries.archivedAt} is null)`)), "Database query failed");
658
682
  const outcomes = [];
659
683
  for (const draft of waiting) {
660
684
  const id = String(draft.id);
661
685
  const principal = options.as(draft.scheduledBy ?? null);
662
686
  const error = yield* config.transaction(Effect.gen(function* () {
663
- if (!isAuthor(principal)) return yield* refuse("Whoever scheduled this is no longer an author");
687
+ if (!(yield* isAuthor(principal, "Update"))) return yield* RemoteServer.forbid("Whoever scheduled this is no longer an author");
664
688
  const found = yield* offering(id, "publish");
665
689
  yield* asking(principal, "publish", found.entry);
666
690
  yield* publishing(found, principal, found.entry.revision);
@@ -698,12 +722,12 @@ const CmsServer = { make: (config) => {
698
722
  archivedAt: null,
699
723
  revision: null
700
724
  };
701
- yield* Effect.promise(() => Promise.resolve(writes.insert(tables.entries).values({
725
+ yield* runStatement(() => writes.insert(tables.entries).values({
702
726
  ...entry,
703
727
  createdBy: nameOf(item.as),
704
728
  createdAt: at.toISOString()
705
- })));
706
- yield* Effect.promise(() => Promise.resolve(writes.insert(tables.drafts).values({
729
+ }), "Database write failed");
730
+ yield* runStatement(() => writes.insert(tables.drafts).values({
707
731
  id,
708
732
  values: item.values,
709
733
  model: null,
@@ -711,7 +735,7 @@ const CmsServer = { make: (config) => {
711
735
  updatedAt: at.toISOString(),
712
736
  updatedBy: nameOf(item.as),
713
737
  baseRevision: null
714
- })));
738
+ }), "Database write failed");
715
739
  return {
716
740
  entry: id,
717
741
  targetId: (yield* publishing({
@@ -733,7 +757,7 @@ const CmsServer = { make: (config) => {
733
757
  yield* asking(principal, "unpublish", entry);
734
758
  const shown = served.type.roles.published;
735
759
  const writes = yield* drizzleWrites;
736
- yield* Effect.promise(() => Promise.resolve(writes.update(served.binding.table).set({ [shown.key]: null }).where(eq(served.binding.columns.id, entry.targetId)).returning({ id: served.binding.columns.id })));
760
+ yield* runStatement(() => writes.update(served.binding.table).set({ [shown.key]: null }).where(eq(served.binding.columns.id, entry.targetId)).returning({ id: served.binding.columns.id }), "Database write failed");
737
761
  const content = returning(served.binding, [shown.key]);
738
762
  return {
739
763
  output: {},
@@ -744,7 +768,7 @@ const CmsServer = { make: (config) => {
744
768
  entries,
745
769
  source(Db.Draft),
746
770
  source(Db.Revision),
747
- ...config.content.map((served) => source(served.binding))
771
+ ...config.content.map((served) => source(served.binding, { filter: GuardServer.filter(served.entity, policy) }))
748
772
  ];
749
773
  const mutations = [
750
774
  SaveDraft,
@@ -757,6 +781,19 @@ const CmsServer = { make: (config) => {
757
781
  archiving("unarchive"),
758
782
  Restore
759
783
  ];
784
+ const slugQueries = config.content.flatMap(({ type, binding }) => type.roles.slug === void 0 ? [] : [{
785
+ descriptor: Cms.bySlug(type),
786
+ binding
787
+ }]);
788
+ const bySlug = slugQueries.map(({ descriptor, binding }) => query(descriptor, { entity: binding }));
789
+ GuardServer.validate(policy, {
790
+ entities: config.content.map((served) => served.entity),
791
+ domain: { registry: { queries: new Map(slugQueries.map(({ descriptor }) => [descriptor.name, descriptor])) } },
792
+ server: RemoteServer.make({
793
+ entities: sources,
794
+ queries: bySlug
795
+ })
796
+ });
760
797
  return {
761
798
  /**
762
799
  * The sources of the content types and of the CMS's own Entities, each behind
@@ -764,7 +801,7 @@ const CmsServer = { make: (config) => {
764
801
  * table: that is how the boundary cannot be forgotten.
765
802
  */
766
803
  sources,
767
- queries: [worklist, ...config.content.flatMap(({ type, binding }) => type.roles.slug === void 0 ? [] : [query(Cms.bySlug(type), { entity: binding })])],
804
+ queries: [worklist, ...bySlug],
768
805
  mutations,
769
806
  due,
770
807
  import: importing,
@@ -773,6 +810,6 @@ const CmsServer = { make: (config) => {
773
810
  };
774
811
  } };
775
812
  //#endregion
776
- export { CmsServer, Transaction, pgTables, published, sqliteSchema, sqliteTables };
813
+ export { CmsServer, Transaction, pgTables, sqliteSchema, sqliteTables };
777
814
 
778
815
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["text","integer","pgText","pgInteger"],"sources":["../src/tables.ts","../src/index.ts"],"sourcesContent":["/**\n * The three tables the CMS keeps beside the application's own. Drizzle tables\n * are made per dialect, so there is one definition for each; they have the same\n * columns. An application adds them to its schema and its migrations as it does\n * any table, and may define its own instead, so long as the columns are these.\n */\nimport { integer as pgInteger, jsonb, pgTable, text as pgText } from 'drizzle-orm/pg-core'\nimport { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\n/**\n * - `cms_entries`: a piece of content from first keystroke to archive. It exists\n * before its content row does.\n * - `cms_drafts`: an entry's one working copy; its id is the entry's.\n * - `cms_revisions`: published values, append-only.\n */\nexport const sqliteTables = () => ({\n entries: sqliteTable('cms_entries', {\n id: text('id').primaryKey(),\n type: text('type').notNull(),\n targetId: text('target_id'),\n label: text('label').notNull(),\n createdBy: text('created_by'),\n createdAt: text('created_at').notNull(),\n archivedAt: text('archived_at'),\n revision: integer('revision'),\n }),\n drafts: sqliteTable('cms_drafts', {\n id: text('id').primaryKey(),\n values: text('values', { mode: 'json' }),\n model: text('model', { mode: 'json' }),\n form: text('form').notNull(),\n updatedAt: text('updated_at').notNull(),\n updatedBy: text('updated_by'),\n baseRevision: integer('base_revision'),\n scheduledFor: text('scheduled_for'),\n scheduledBy: text('scheduled_by'),\n scheduleError: text('schedule_error'),\n }),\n revisions: sqliteTable('cms_revisions', {\n id: text('id').primaryKey(),\n entryId: text('entry_id').notNull(),\n n: integer('n').notNull(),\n values: text('values', { mode: 'json' }),\n publishedAt: text('published_at').notNull(),\n publishedBy: text('published_by'),\n }),\n})\n\nexport const pgTables = () => ({\n entries: pgTable('cms_entries', {\n id: pgText('id').primaryKey(),\n type: pgText('type').notNull(),\n targetId: pgText('target_id'),\n label: pgText('label').notNull(),\n createdBy: pgText('created_by'),\n createdAt: pgText('created_at').notNull(),\n archivedAt: pgText('archived_at'),\n revision: pgInteger('revision'),\n }),\n drafts: pgTable('cms_drafts', {\n id: pgText('id').primaryKey(),\n values: jsonb('values'),\n model: jsonb('model'),\n form: pgText('form').notNull(),\n updatedAt: pgText('updated_at').notNull(),\n updatedBy: pgText('updated_by'),\n baseRevision: pgInteger('base_revision'),\n scheduledFor: pgText('scheduled_for'),\n scheduledBy: pgText('scheduled_by'),\n scheduleError: pgText('schedule_error'),\n }),\n revisions: pgTable('cms_revisions', {\n id: pgText('id').primaryKey(),\n entryId: pgText('entry_id').notNull(),\n n: pgInteger('n').notNull(),\n values: jsonb('values'),\n publishedAt: pgText('published_at').notNull(),\n publishedBy: pgText('published_by'),\n }),\n})\n\n/** The `create table` statements for SQLite, for a test or a first migration. */\nexport const sqliteSchema = `\ncreate table if not exists cms_entries (\n id text primary key, type text not null, target_id text, label text not null,\n created_by text, created_at text not null, archived_at text, revision integer\n);\ncreate table if not exists cms_drafts (\n id text primary key, \"values\" text, model text, form text not null,\n updated_at text not null, updated_by text, base_revision integer,\n scheduled_for text, scheduled_by text, schedule_error text\n);\ncreate table if not exists cms_revisions (\n id text primary key, entry_id text not null, n integer not null, \"values\" text,\n published_at text not null, published_by text\n);\n`\n\nexport type CmsTables = ReturnType<typeof sqliteTables> | ReturnType<typeof pgTables>\n","/**\n * `foldkit-cms-drizzle` — the server half of `foldkit-cms`.\n *\n * It keeps an author's unpublished work in three tables beside the\n * application's own, and it is where the audience boundary is enforced: a\n * principal that is not an author is refused entries, drafts and revisions\n * outright, and sees of the content only the rows its `published` role shows.\n * Both are a binding's `visible`, so they hold on every path a table is read by.\n *\n * Publishing runs the application's own mutation with the draft's value, inside\n * a transaction that also appends the revision and removes the draft, so a\n * publish happened entirely or did not happen.\n */\nimport {\n and,\n eq,\n inArray,\n isNotNull,\n isNull,\n lte,\n ne,\n sql,\n type AnyColumn,\n type SQL,\n} from 'drizzle-orm'\nimport { Effect, Exit, Schema, Semaphore } from 'effect'\nimport { Cms, type Content, type Facts, type Transition } from 'foldkit-cms'\nimport type { MutationDescriptor } from 'foldkit-remote'\nimport {\n DrizzleDatabase,\n drizzleWrites,\n bind,\n query,\n returning,\n source,\n type AnyEntityBinding,\n type DrizzleDatabaseService,\n type Visible,\n} from 'foldkit-remote-drizzle'\nimport {\n RemoteServer,\n RemoteServerError,\n type EntitySource,\n type MutationOutcome,\n type MutationSource,\n type QuerySource,\n} from 'foldkit-remote-server'\nimport type { CmsTables } from './tables.js'\n\nexport { pgTables, sqliteSchema, sqliteTables, type CmsTables } from './tables.js'\n\n/**\n * A content type as the server holds it: its declaration, the binding of its\n * table, and the application's own handlers of its two publish mutations. What\n * publishing a post does stays the application's code.\n */\nexport interface ServedContent<P = any> {\n readonly type: Content<string, any, any, any>\n readonly binding: AnyEntityBinding\n readonly create: MutationSource<P, DrizzleDatabase>\n readonly update: MutationSource<P, DrizzleDatabase>\n}\n\n/**\n * Runs some work so that it happened entirely or did not. Drizzle's own\n * transactions differ by driver, so the application says which this is.\n */\nexport type Transaction = <A, E>(\n work: Effect.Effect<A, E, DrizzleDatabase>,\n) => Effect.Effect<A, E, DrizzleDatabase>\n\nconst statement = (database: DrizzleDatabaseService, text: string) =>\n Effect.promise(() => {\n const runs = database as unknown as {\n run?: (query: SQL) => unknown\n execute?: (query: SQL) => unknown\n }\n return Promise.resolve(\n runs.run !== undefined ? runs.run(sql.raw(text)) : runs.execute!(sql.raw(text)),\n )\n })\n\n// One connection holds one transaction. Without this, a write another request\n// makes between this one's `begin` and its `rollback` is rolled back with it,\n// after it was reported done.\nconst turns = new WeakMap<object, Semaphore.Semaphore>()\nconst turnOf = (database: object): Semaphore.Semaphore => {\n const held = turns.get(database)\n if (held !== undefined) return held\n const made = Semaphore.makeUnsafe(1)\n turns.set(database, made)\n return made\n}\n\nexport const Transaction = {\n /**\n * `begin`, the work, then `commit` or `rollback`, as statements. For a database\n * that is one connection, such as a SQLite file, which it takes turns at: the\n * operations of this package wait for one another. A write of your own on the\n * same connection does not, so make it inside `Transaction.statements` too.\n * Over a pool each statement may take a different connection: use `drizzle` there.\n */\n statements: (<A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n return yield* turnOf(database).withPermit(\n statement(database, 'begin').pipe(\n Effect.andThen(work),\n Effect.onExit(exit => statement(database, Exit.isSuccess(exit) ? 'commit' : 'rollback')),\n ),\n )\n })) as Transaction,\n /**\n * Drizzle's `database.transaction`, with the work given the transaction as its\n * database. For a driver whose transactions are asynchronous: Postgres, libSQL.\n */\n drizzle: (<A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) =>\n Effect.gen(function* () {\n const database = (yield* DrizzleDatabase) as unknown as {\n transaction: <T>(run: (inner: DrizzleDatabaseService) => Promise<T>) => Promise<T>\n }\n const failed = Symbol('failed')\n let exit: Exit.Exit<A, E> | undefined\n // A failure must leave the callback as a rejection, or Drizzle commits.\n yield* Effect.promise(() =>\n database\n .transaction(async inner => {\n exit = await Effect.runPromiseExit(\n work.pipe(Effect.provideService(DrizzleDatabase, inner)),\n )\n if (!Exit.isSuccess(exit)) throw failed\n })\n .catch(error => {\n if (error !== failed) throw error\n }),\n )\n return yield* exit!\n })) as Transaction,\n}\n\nexport interface CmsServerConfig<P> {\n readonly tables: CmsTables\n readonly content: ReadonlyArray<ServedContent<P>>\n /** How a publish is made whole: `Transaction.statements` or `Transaction.drizzle`. */\n readonly transaction: Transaction\n /** Who reads and writes unpublished work. Everyone else is a visitor. */\n readonly isAuthor: (principal: P) => boolean\n /**\n * Whether this author may make this transition. It is asked after the entry is\n * found and the transition is one its state offers. Default: any author may.\n */\n readonly allow?: (principal: P, transition: Transition, entry: EntryRow) => boolean\n /** The server's clock, passed in so a test can hold it. */\n readonly now?: () => Date\n /**\n * The most a draft may hold, as the characters of its JSON: its values and its\n * form's Model together. An author is trusted with their own content, not with\n * the database's disk. Default: one million.\n */\n readonly maxDraftSize?: number\n /** Who a principal is, for `createdBy` and `updatedBy`. */\n readonly nameOf?: (principal: P) => string | null\n}\n\n/** A row of `cms_entries`, as the server's rules see it. */\nexport interface EntryRow {\n readonly id: string\n readonly type: string\n readonly targetId: string | null\n readonly label: string\n readonly archivedAt: string | null\n readonly revision: number | null\n}\n\n/**\n * One import: content published already, such as a site's first pages or what\n * another CMS held. `as` is whom it is imported as (an editor, so the create\n * handler writes the row); a runner that imports many fixes `as` once and\n * varies the rest. `at` is when it was published (default now); `entry`\n * names the entry (default a new id) — name it where another import points\n * at it, and import what is pointed at first.\n */\nexport interface ImportItem<P> {\n readonly type: string\n readonly values: unknown\n readonly as: P\n readonly at?: Date | undefined\n readonly entry?: string | undefined\n}\n\nconst refuse = (message: string) => new RemoteServerError({ message })\n\n/**\n * Whether a unique index on this column refused a write. Drivers say so in their\n * own words, and Drizzle wraps what they say, so every message down the chain of\n * causes is read. SQLite names the column; Postgres names the constraint, which\n * by default is made of the column's name.\n */\nconst refusedAsDuplicate = (defect: unknown, column: string): boolean => {\n const said: Array<string> = []\n for (let at = defect, depth = 0; at != null && depth < 5; depth++) {\n const { message, constraint, cause } = at as {\n readonly message?: unknown\n readonly constraint?: unknown\n readonly cause?: unknown\n }\n // Drizzle's own message quotes the statement, which names every column.\n if (!String(message).startsWith('Failed query'))\n said.push(String(message ?? at), String(constraint ?? ''))\n at = cause\n }\n const text = said.join(' ')\n const named = new RegExp(`(^|[^a-zA-Z0-9])${column.replace(/[^\\w]/g, '.')}([^a-zA-Z0-9]|$)`)\n return /unique|duplicate/i.test(text) && named.test(text)\n}\n\n/**\n * For a content table's binding: a visitor sees the rows whose `published`\n * column is set, and an author sees every row. Give it as the storage's\n * `visible` when the table is bound; `CmsServer.make` refuses a content type\n * that can be unpublished and whose binding shows every row to everyone.\n */\nexport const published =\n <P>(column: AnyColumn, isAuthor: (principal: P) => boolean): Visible =>\n principal =>\n isAuthor(principal as P) ? undefined : isNotNull(column)\n\nexport const CmsServer = {\n make: <P = unknown>(config: CmsServerConfig<P>) => {\n const { tables, isAuthor } = config\n const now = config.now ?? (() => new Date())\n const nameOf = config.nameOf ?? (() => null)\n const maxDraftSize = config.maxDraftSize ?? 1_000_000\n\n const byType = new Map(config.content.map(served => [served.type.name, served]))\n for (const { type, binding, create, update } of config.content) {\n for (const [handler, declared] of [\n [create, type.publish.create],\n [update, type.publish.update],\n ] as const)\n if (handler.mutation !== declared.name)\n throw new Error(\n `foldkit-cms-drizzle: content \"${type.name}\" publishes through \"${declared.name}\", but was given the handler of \"${handler.mutation}\"`,\n )\n if (type.roles.published === undefined) continue\n // Whether the rule is right is the application's to say: only it knows what a\n // principal is. That there is one at all is checked here.\n if (binding.visible === undefined)\n throw new Error(\n `foldkit-cms-drizzle: content \"${type.name}\" can be unpublished, but its binding shows every row to everyone; bind its table with \\`visible: published(column, isAuthor)\\``,\n )\n }\n\n // Unpublished work is an author's. To anyone else these tables are empty, on\n // every path they are read by.\n const authorsOnly: Visible = principal => (isAuthor(principal as P) ? undefined : sql`0 = 1`)\n const Db = bind(Cms.Entities, {\n Entry: {\n table: tables.entries,\n visible: authorsOnly,\n relations: {\n draft: { foreignKey: tables.drafts.id },\n revisions: {\n foreignKey: tables.revisions.entryId,\n orderBy: [{ column: tables.revisions.n, direction: 'desc' }],\n },\n },\n // Not a column: this server derives it, below, with its own clock.\n derived: { state: { supplied: true }, may: { supplied: true } },\n },\n Draft: { table: tables.drafts, visible: authorsOnly },\n Revision: { table: tables.revisions, visible: authorsOnly },\n })\n\n /** What is known of some entries, for their state: their drafts, and whether a visitor sees their rows. */\n const factsOf = (entries: ReadonlyArray<EntryRow>) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const ids = entries.map(entry => entry.id)\n const drafts =\n ids.length === 0\n ? []\n : yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select({\n id: tables.drafts.id,\n scheduledFor: tables.drafts.scheduledFor,\n scheduleError: tables.drafts.scheduleError,\n })\n .from(tables.drafts)\n .where(inArray(tables.drafts.id, ids)),\n ),\n )\n const draftOf = new Map(drafts.map(draft => [String(draft.id), draft]))\n\n // Per content type, which target rows exist and which a visitor would see.\n const rows = new Map<string, 'visible' | 'hidden'>()\n for (const [name, { type, binding }] of byType) {\n const targets = entries.flatMap(entry =>\n entry.type === name && entry.targetId !== null ? [entry.targetId] : [],\n )\n if (targets.length === 0) continue\n const id = binding.columns.id!\n const publishedColumn =\n type.roles.published === undefined\n ? undefined\n : binding.columns[type.roles.published.key]\n const found = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select({\n id,\n ...(publishedColumn === undefined ? {} : { shown: publishedColumn }),\n })\n .from(binding.table)\n .where(inArray(id, targets)),\n ),\n )\n for (const row of found)\n rows.set(\n `${name}:${String(row.id)}`,\n publishedColumn === undefined || row.shown !== null ? 'visible' : 'hidden',\n )\n }\n\n return new Map(\n entries.map((entry): readonly [string, Facts] => {\n const draft = draftOf.get(entry.id)\n return [\n entry.id,\n {\n archivedAt: entry.archivedAt,\n row:\n entry.targetId === null\n ? 'none'\n : (rows.get(`${entry.type}:${entry.targetId}`) ?? 'none'),\n draft:\n draft === undefined\n ? null\n : {\n scheduledFor: (draft.scheduledFor as string | null) ?? null,\n scheduleError: (draft.scheduleError as string | null) ?? null,\n },\n },\n ]\n }),\n )\n })\n\n const entryColumns = {\n id: tables.entries.id,\n type: tables.entries.type,\n targetId: tables.entries.targetId,\n label: tables.entries.label,\n archivedAt: tables.entries.archivedAt,\n revision: tables.entries.revision,\n }\n const findEntry = (id: string) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const [row] = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select(entryColumns)\n .from(tables.entries)\n .where(eq(tables.entries.id, id))\n .limit(1),\n ),\n )\n return row as EntryRow | undefined\n })\n\n // An entry's `state` and `may` are not columns. The generated source reads the\n // rest; the state is derived from what is known of each entry, with this server's\n // clock, and `may` is what `allow` lets the reader ask of it.\n const generated = source<P>(Db.Entry)\n const entries: EntitySource<P, DrizzleDatabase> = {\n ...generated,\n read: context =>\n Effect.gen(function* () {\n const wantsState = context.fields.includes('state')\n const wantsMay = context.fields.includes('may')\n const fields = context.fields.filter(field => field !== 'state' && field !== 'may')\n const records = yield* generated.read({\n ...context,\n // Both need these of each entry, asked for or not; `allow` is given the row.\n fields:\n wantsState || wantsMay\n ? [\n ...new Set([\n ...fields,\n 'type',\n 'targetId',\n 'archivedAt',\n ...(wantsMay ? ['label', 'revision'] : []),\n ]),\n ]\n : fields,\n })\n if (!wantsState && !wantsMay) return records\n const rows = records.map((record): EntryRow => ({\n id: record.id,\n type: String(record.values.type),\n targetId: (record.values.targetId as string | null) ?? null,\n label: String(record.values.label ?? ''),\n archivedAt: (record.values.archivedAt as string | null) ?? null,\n revision: (record.values.revision as number | null) ?? null,\n }))\n const facts = yield* factsOf(rows)\n const at = now()\n return records.map((record, index) => {\n const row = rows[index]!\n const known = facts.get(record.id)!\n return {\n id: record.id,\n values: {\n ...Object.fromEntries(\n Object.entries(record.values).filter(\n ([field]) => field === 'id' || context.fields.includes(field),\n ),\n ),\n ...(wantsState ? { state: Cms.state(known, at) } : {}),\n // What `allow` lets this reader ask of this entry, whatever its state\n // offers now: the state says that, and a draft saved a moment later\n // changes it without asking the server again.\n ...(wantsMay\n ? {\n may: Cms.transitions.filter(\n transition => config.allow?.(context.principal, transition, row) !== false,\n ),\n }\n : {}),\n },\n }\n })\n }),\n }\n\n // The descriptor carries all three questions and the order; this says only\n // which table answers them. The audience boundary is unchanged: `visible`\n // on the binding is conjoined with the body, as for every source here.\n const worklist: QuerySource<P, DrizzleDatabase> = query(Cms.Entries, { entity: Db.Entry })\n\n /** A mutation of this server: its principal and its database are fixed, its input is the descriptor's. */\n const operation = <Name extends string, Input, Output>(\n descriptor: MutationDescriptor<Name, Input, Output>,\n run: (context: {\n readonly input: Input\n readonly principal: P\n }) => Effect.Effect<MutationOutcome<Output>, RemoteServerError, DrizzleDatabase>,\n ): MutationSource<P, DrizzleDatabase> =>\n // Asked before anything is looked up: a visitor is not told which entries there are.\n // Then the whole of it is one transaction: a save that made an entry and\n // could not make its draft leaves neither.\n RemoteServer.mutation<P, DrizzleDatabase, Name, Input, Output>(descriptor, context =>\n isAuthor(context.principal)\n ? config.transaction(run(context))\n : Effect.fail(refuse('Only an author may change unpublished work')),\n )\n\n /**\n * A scheduled draft is published, when its time comes, as whoever scheduled it.\n * So changing it, replacing it or removing it is for someone who could have\n * scheduled it: otherwise an author who may not publish would publish through\n * someone who may.\n */\n const promised = (principal: P, entry: EntryRow) =>\n Effect.gen(function* () {\n const [draft] = yield* readRows(\n Db.Draft,\n { scheduledFor: tables.drafts.scheduledFor },\n entry.id,\n )\n if (draft?.scheduledFor == null) return\n if (config.allow?.(principal, 'schedule', entry) === false)\n return yield* refuse(\n 'This draft is scheduled, and only someone who may schedule it may change it',\n )\n })\n\n /** Whether the application lets this author do this, once the entry is known. */\n const asking = (principal: P, transition: Transition, entry: EntryRow | undefined) =>\n entry !== undefined && config.allow?.(principal, transition, entry) === false\n ? Effect.fail(refuse(`This author may not ${transition} this entry`))\n : Effect.void\n\n const draftFields = ['values', 'model', 'form', 'updatedAt', 'updatedBy', 'baseRevision']\n const draftPatch = returning(Db.Draft, draftFields)\n const entryPatch = returning(Db.Entry, [\n 'type',\n 'targetId',\n 'label',\n 'createdAt',\n 'archivedAt',\n 'revision',\n ])\n /**\n * The entry as the client should now hold it, its state with it: an operation\n * changes the state, and a patch without it would leave the old one on screen.\n */\n const entryPatches = (id: string) =>\n Effect.gen(function* () {\n const patches = entryPatch.patches(yield* readRows(Db.Entry, entryPatch.columns, id))\n const entry = yield* findEntry(id)\n if (entry === undefined) return patches\n const facts = (yield* factsOf([entry])).get(id)!\n return patches.map(patch => ({\n ...patch,\n values: { ...patch.values, state: Cms.state(facts, now()) },\n }))\n })\n const readRows = (binding: AnyEntityBinding, columns: Record<string, AnyColumn>, id: string) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n return yield* Effect.promise(() =>\n Promise.resolve(\n database.select(columns).from(binding.table).where(eq(binding.columns.id!, id)),\n ),\n )\n })\n\n const SaveDraft = operation(Cms.Operations.SaveDraft, ({ input, principal }) =>\n Effect.gen(function* () {\n if (!byType.has(input.type))\n return yield* refuse(`\"${input.type}\" is not a type of content this server knows`)\n const size = JSON.stringify([input.values, input.model]).length\n if (size > maxDraftSize)\n return yield* refuse(\n `This draft is too large to save: ${size} characters, and ${maxDraftSize} is the most`,\n )\n // An id nobody has is something new: the client named it, and this makes it.\n const existing = yield* findEntry(input.entry)\n if (existing !== undefined && existing.type !== input.type)\n return yield* refuse(`This entry is of \"${existing.type}\", not \"${input.type}\"`)\n if (existing?.archivedAt != null) return yield* refuse('An archived entry takes no draft')\n yield* asking(principal, 'save', existing)\n if (existing !== undefined) yield* promised(principal, existing)\n\n const database = yield* drizzleWrites\n const who = nameOf(principal)\n const id = existing?.id ?? input.entry\n const [held] = yield* readRows(Db.Draft, { updatedAt: tables.drafts.updatedAt }, id)\n const previous = held === undefined ? null : String(held.updatedAt)\n // Two saves in one millisecond must still be told apart by the next one.\n const at = now().toISOString()\n const updatedAt =\n previous !== null && at <= previous\n ? new Date(new Date(previous).getTime() + 1).toISOString()\n : at\n const written = {\n values: input.values,\n model: input.model,\n form: input.form,\n updatedAt,\n updatedBy: who,\n // A draft that changed is worth trying again when its time comes.\n scheduleError: null,\n }\n\n // The first draft of an entry has nothing to have been based on. Asked before\n // anything is written, so a refusal leaves no entry behind.\n if (previous === null && input.basedOn !== null)\n return yield* refuse('CmsConflict: this draft was discarded')\n if (existing === undefined) {\n yield* Effect.promise(() =>\n Promise.resolve(\n database.insert(tables.entries).values({\n id,\n type: input.type,\n targetId: null,\n label: input.label,\n createdBy: who,\n createdAt: at,\n archivedAt: null,\n revision: null,\n }),\n ),\n )\n }\n if (previous === null) {\n yield* Effect.promise(() =>\n Promise.resolve(\n database.insert(tables.drafts).values({ id, ...written, baseRevision: null }),\n ),\n )\n } else {\n // Compare and set: the save names what it was made from, and a newer one\n // on the server means someone else saved in between.\n const changed = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .update(tables.drafts)\n .set(written)\n .where(\n and(eq(tables.drafts.id, id), eq(tables.drafts.updatedAt, input.basedOn ?? '')),\n )\n .returning({ id: tables.drafts.id }),\n ),\n )\n if (changed.length === 0)\n return yield* refuse('CmsConflict: this draft was saved by someone else since')\n }\n if (existing !== undefined && existing.label !== input.label) {\n yield* Effect.promise(() =>\n Promise.resolve(\n database\n .update(tables.entries)\n .set({ label: input.label })\n .where(eq(tables.entries.id, id))\n .returning({ id: tables.entries.id }),\n ),\n )\n }\n\n const entities = [\n ...(yield* entryPatches(id)),\n ...draftPatch.patches(yield* readRows(Db.Draft, draftPatch.columns, id)),\n ]\n return { output: { entry: id as never, updatedAt }, entities }\n }),\n )\n\n const DiscardDraft = operation(Cms.Operations.DiscardDraft, ({ input, principal }) =>\n Effect.gen(function* () {\n const entry = yield* findEntry(input.entry)\n if (entry === undefined) return yield* refuse('There is no such entry')\n yield* asking(principal, 'discard', entry)\n yield* promised(principal, entry)\n const database = yield* drizzleWrites\n yield* Effect.promise(() =>\n Promise.resolve(database.delete(tables.drafts).where(eq(tables.drafts.id, entry.id))),\n )\n const gone = [{ entity: 'CmsDraft', id: entry.id }]\n // Never published, and now with nothing entered: there is nothing left of it.\n if (entry.targetId !== null)\n return { output: {}, deleted: gone, entities: yield* entryPatches(entry.id) }\n yield* Effect.promise(() =>\n Promise.resolve(database.delete(tables.entries).where(eq(tables.entries.id, entry.id))),\n )\n return { output: {}, deleted: [...gone, { entity: 'CmsEntry', id: entry.id }] }\n }),\n )\n\n const done: Readonly<Record<Exclude<Transition, 'save' | 'discard'>, string>> = {\n publish: 'published',\n unpublish: 'unpublished',\n schedule: 'scheduled',\n unschedule: 'unscheduled',\n archive: 'archived',\n unarchive: 'unarchived',\n restore: 'restored',\n }\n /** The entry, if its state offers this transition now. */\n const offering = (id: string, transition: Exclude<Transition, 'save' | 'discard'>) =>\n Effect.gen(function* () {\n const entry = yield* findEntry(id)\n if (entry === undefined) return yield* refuse('There is no such entry')\n const served = byType.get(entry.type)\n if (served === undefined)\n return yield* refuse(`\"${entry.type}\" is not a type of content this server knows`)\n const facts = (yield* factsOf([entry])).get(entry.id)!\n if (!Cms.offers(facts, now(), served.type).includes(transition))\n return yield* refuse(`This entry cannot be ${done[transition]} as it stands`)\n return { entry, served, facts }\n })\n\n /** What the `published` role's column holds for \"now\". */\n const stamp = (column: AnyColumn, at: Date) => {\n const kind = column.dataType.split(' ')[0]\n return kind === 'string' ? at.toISOString() : kind === 'number' ? at.getTime() : at\n }\n\n const scheduledPatch = returning(Db.Draft, ['scheduledFor', 'scheduleError'])\n const revisionPatch = returning(Db.Revision, ['n', 'values', 'publishedAt', 'publishedBy'])\n\n type Found = Effect.Success<ReturnType<typeof offering>>\n\n /** The draft as the publish mutation would take it, or why it would not. */\n const readied = ({ entry, served, facts }: Found) =>\n Effect.gen(function* () {\n const [draft] = yield* readRows(Db.Draft, { values: tables.drafts.values }, entry.id)\n // A row that was deleted under its entry is made again.\n const creating = facts.row === 'none'\n const handler = creating ? served.create : served.update\n const value =\n creating || typeof draft?.values !== 'object' || draft.values === null\n ? draft?.values\n : { ...draft.values, id: entry.targetId }\n const decoded = yield* Schema.decodeUnknownEffect(handler.Input)(value).pipe(\n Effect.mapError(error => refuse(`This draft is not ready to publish: ${String(error)}`)),\n )\n return { draft, creating, handler, decoded }\n })\n\n /** A publish, for the operation that asks for one, the schedule that comes due, and an import. */\n const publishing = (found: Found, principal: P, basedOn: number | null, at: Date = now()) =>\n Effect.gen(function* () {\n const { entry, served } = found\n const database = yield* DrizzleDatabase\n const held = entry.revision\n const conflict = refuse('CmsConflict: this entry was published by someone else since')\n if (held !== basedOn) return yield* conflict\n const { draft, creating, handler, decoded } = yield* readied(found)\n\n // A taken slug is refused by name, on its key. This is advice: two publishes\n // can both pass it, and then the unique index is the rule, below.\n const address = served.type.roles.slug\n const slug =\n address === undefined\n ? undefined\n : (decoded as Readonly<Record<string, unknown>> | null)?.[address.key]\n const taken = (key: string) => refuse(Cms.slugTaken.message(key, String(slug)))\n if (address !== undefined && typeof slug === 'string') {\n const column = served.binding.columns[address.key]!\n const id = served.binding.columns.id!\n const others = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select({ id })\n .from(served.binding.table)\n .where(and(eq(column, slug), creating ? undefined : ne(id, entry.targetId!)))\n .limit(1),\n ),\n )\n if (others.length > 0) return yield* taken(address.key)\n }\n\n return yield* Effect.gen(function* () {\n const writes = yield* drizzleWrites\n const n = (held ?? 0) + 1\n // Compare and set, first: of two publishes made from one revision, one\n // finds the number already moved, and nothing of it is written.\n const moved = yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(tables.entries)\n .set({ revision: n })\n .where(\n and(\n eq(tables.entries.id, entry.id),\n held === null\n ? isNull(tables.entries.revision)\n : eq(tables.entries.revision, held),\n ),\n )\n .returning({ id: tables.entries.id }),\n ),\n )\n if (moved.length === 0) return yield* conflict\n const ran = yield* handler.run({ input: decoded, principal })\n const targetId = creating\n ? String((ran.output as { readonly id: unknown }).id)\n : entry.targetId!\n const shown = served.type.roles.published\n if (shown !== undefined) {\n // Publishing shows the row. One already shown keeps its first date.\n const column = served.binding.columns[shown.key]!\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(served.binding.table)\n .set({ [shown.key]: stamp(column, at) })\n .where(and(eq(served.binding.columns.id!, targetId), isNull(column)))\n .returning({ id: served.binding.columns.id! }),\n ),\n )\n }\n const revisionId = `${entry.id}:${n}`\n yield* Effect.promise(() =>\n Promise.resolve(\n writes.insert(tables.revisions).values({\n id: revisionId,\n entryId: entry.id,\n n,\n values: draft?.values,\n publishedAt: at.toISOString(),\n publishedBy: nameOf(principal),\n }),\n ),\n )\n yield* Effect.promise(() =>\n Promise.resolve(writes.delete(tables.drafts).where(eq(tables.drafts.id, entry.id))),\n )\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(tables.entries)\n .set({ targetId })\n .where(eq(tables.entries.id, entry.id))\n .returning({ id: tables.entries.id }),\n ),\n )\n return {\n output: { entry: entry.id as never, targetId, revision: n },\n entities: [\n ...ran.entities,\n // The row as the handler left it, every column: a handler need not patch what it wrote.\n ...(yield* returning.row(served.binding, targetId)),\n ...(yield* entryPatches(entry.id)),\n ...revisionPatch.patches(\n yield* readRows(Db.Revision, revisionPatch.columns, revisionId),\n ),\n ],\n connections: ran.connections,\n deleted: [...ran.deleted, { entity: 'CmsDraft', id: entry.id }],\n }\n }).pipe(\n // The publish that lost the race: the index refused it, and it is the same\n // news as the check's. Any other defect is left as it was.\n Effect.catchDefect(defect =>\n address !== undefined &&\n refusedAsDuplicate(defect, served.binding.columns[address.key]!.name)\n ? Effect.fail(taken(address.key))\n : Effect.die(defect),\n ),\n )\n })\n\n const Publish = operation(Cms.Operations.Publish, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'publish')\n yield* asking(principal, 'publish', found.entry)\n return yield* publishing(found, principal, input.basedOn)\n }),\n )\n\n /** Writes to an entry's draft, and answers with both as the client should now hold them. */\n const drafted = (id: string, values: object) =>\n Effect.gen(function* () {\n const writes = yield* drizzleWrites\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(tables.drafts)\n .set(values)\n .where(eq(tables.drafts.id, id))\n .returning({ id: tables.drafts.id }),\n ),\n )\n return {\n output: {},\n entities: [\n ...(yield* entryPatches(id)),\n ...scheduledPatch.patches(yield* readRows(Db.Draft, scheduledPatch.columns, id)),\n ],\n }\n })\n\n const Schedule = operation(Cms.Operations.Schedule, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'schedule')\n yield* asking(principal, 'schedule', found.entry)\n if (Number.isNaN(new Date(input.at).getTime()))\n return yield* refuse(`\"${input.at}\" is not a time`)\n // What cannot be published now is not promised for later.\n yield* readied(found)\n return yield* drafted(found.entry.id, {\n scheduledFor: new Date(input.at).toISOString(),\n scheduledBy: nameOf(principal),\n scheduleError: null,\n })\n }),\n )\n\n const Unschedule = operation(Cms.Operations.Unschedule, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'unschedule')\n yield* asking(principal, 'unschedule', found.entry)\n return yield* drafted(found.entry.id, {\n scheduledFor: null,\n scheduledBy: null,\n scheduleError: null,\n })\n }),\n )\n\n /** Archiving and its undoing: a fact about the entry, and for what can be hidden, the row. */\n const archiving = (transition: 'archive' | 'unarchive') =>\n operation(\n transition === 'archive' ? Cms.Operations.Archive : Cms.Operations.Unarchive,\n ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry, served } = yield* offering(input.entry, transition)\n yield* asking(principal, transition, entry)\n const writes = yield* drizzleWrites\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(tables.entries)\n .set({ archivedAt: transition === 'archive' ? now().toISOString() : null })\n .where(eq(tables.entries.id, entry.id))\n .returning({ id: tables.entries.id }),\n ),\n )\n const shown = served.type.roles.published\n if (transition === 'unarchive' || shown === undefined || entry.targetId === null)\n return { output: {}, entities: yield* entryPatches(entry.id) }\n // What is put away is not left on show. It comes back as unpublished work.\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(served.binding.table)\n .set({ [shown.key]: null })\n .where(eq(served.binding.columns.id!, entry.targetId!))\n .returning({ id: served.binding.columns.id! }),\n ),\n )\n const content = returning(served.binding, [shown.key])\n return {\n output: {},\n entities: [\n ...content.patches(\n yield* readRows(served.binding, content.columns, entry.targetId),\n ),\n ...(yield* entryPatches(entry.id)),\n ],\n }\n }),\n )\n\n /**\n * A revision's value becomes the working copy, and nothing is published. It\n * replaces what the draft held, and takes back a promise made of that: what\n * was scheduled is not what is there now. The saved Model goes too, so an\n * editor fills its form from the values, key by key.\n */\n const Restore = operation(Cms.Operations.Restore, ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry } = yield* offering(input.entry, 'restore')\n yield* asking(principal, 'restore', entry)\n yield* promised(principal, entry)\n const database = yield* DrizzleDatabase\n const [revision] = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select({ values: tables.revisions.values })\n .from(tables.revisions)\n .where(\n and(eq(tables.revisions.entryId, entry.id), eq(tables.revisions.n, input.revision)),\n )\n .limit(1),\n ),\n )\n if (revision === undefined)\n return yield* refuse(`This entry has no revision ${input.revision}`)\n\n const writes = yield* drizzleWrites\n const [held] = yield* readRows(Db.Draft, { updatedAt: tables.drafts.updatedAt }, entry.id)\n const at = now().toISOString()\n const updatedAt =\n held !== undefined && at <= String(held.updatedAt)\n ? new Date(new Date(String(held.updatedAt)).getTime() + 1).toISOString()\n : at\n const restored = {\n values: revision.values,\n model: null,\n form: `restored@${input.revision}`,\n updatedAt,\n updatedBy: nameOf(principal),\n baseRevision: entry.revision,\n scheduledFor: null,\n scheduledBy: null,\n scheduleError: null,\n }\n yield* Effect.promise(() =>\n Promise.resolve(\n held === undefined\n ? writes.insert(tables.drafts).values({ id: entry.id, ...restored })\n : writes\n .update(tables.drafts)\n .set(restored)\n .where(eq(tables.drafts.id, entry.id))\n .returning({ id: tables.drafts.id }),\n ),\n )\n return {\n output: { updatedAt },\n entities: [\n ...(yield* entryPatches(entry.id)),\n ...draftPatch.patches(yield* readRows(Db.Draft, draftPatch.columns, entry.id)),\n ...scheduledPatch.patches(yield* readRows(Db.Draft, scheduledPatch.columns, entry.id)),\n ],\n }\n }),\n )\n\n /**\n * Publishes every draft whose time has come, each in its own transaction, as\n * whoever scheduled it: `as` says who a stored name is. One that fails stays\n * scheduled with the reason, and is left alone until its draft changes, so a\n * fault the author must fix is not tried again every minute. The package owns\n * no timer: a cron trigger, an interval or a queue calls this.\n */\n const due = (at: Date, options: { readonly as: (scheduledBy: string | null) => P }) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const waiting = yield* Effect.promise(() =>\n Promise.resolve(\n database\n .select({ id: tables.drafts.id, scheduledBy: tables.drafts.scheduledBy })\n .from(tables.drafts)\n .where(\n and(\n isNotNull(tables.drafts.scheduledFor),\n lte(tables.drafts.scheduledFor, at.toISOString()),\n isNull(tables.drafts.scheduleError),\n // What is put away keeps its promise, and keeps it waiting.\n sql`${tables.drafts.id} in (select ${tables.entries.id} from ${tables.entries} where ${tables.entries.archivedAt} is null)`,\n ),\n ),\n ),\n )\n const outcomes: Array<{ readonly entry: string; readonly error: string | null }> = []\n for (const draft of waiting) {\n const id = String(draft.id)\n const principal = options.as((draft.scheduledBy as string | null) ?? null)\n const error = yield* config\n .transaction(\n Effect.gen(function* () {\n if (!isAuthor(principal))\n return yield* refuse('Whoever scheduled this is no longer an author')\n const found = yield* offering(id, 'publish')\n yield* asking(principal, 'publish', found.entry)\n yield* publishing(found, principal, found.entry.revision)\n return null\n }),\n )\n .pipe(\n Effect.catch(failure => Effect.succeed(failure.message)),\n Effect.catchDefect(defect => Effect.succeed(String(defect))),\n )\n // Recorded after the failed publish was undone, or it would be undone with it.\n if (error !== null) yield* config.transaction(drafted(id, { scheduleError: error }))\n outcomes.push({ entry: id, error })\n }\n return outcomes\n })\n\n /**\n * Content published already, such as a site's first pages or what another CMS\n * held, written by the path a publish takes: an entry and a draft of\n * `values`, then the publish, which runs the type's own `create` handler as\n * `as` and records revision 1. It is the server's act, not an author's, so\n * `allow` is not asked. `at` is when it was published (default now); `entry`\n * names the entry (default a new id). One transaction: it all lands or none.\n */\n const importing = (item: ImportItem<P>) =>\n config.transaction(\n Effect.gen(function* () {\n const served = byType.get(item.type)\n if (served === undefined)\n return yield* refuse(`\"${item.type}\" is not a type of content this server knows`)\n const writes = yield* drizzleWrites\n const id = item.entry ?? Cms.newEntryId()\n const at = item.at ?? now()\n const labelKey = served.type.roles.label?.key\n const named =\n labelKey === undefined\n ? undefined\n : (item.values as Readonly<Record<string, unknown>> | null)?.[labelKey]\n const entry: EntryRow = {\n id,\n type: item.type,\n targetId: null,\n label: typeof named === 'string' ? named : '',\n archivedAt: null,\n revision: null,\n }\n yield* Effect.promise(() =>\n Promise.resolve(\n writes.insert(tables.entries).values({\n ...entry,\n createdBy: nameOf(item.as),\n createdAt: at.toISOString(),\n }),\n ),\n )\n yield* Effect.promise(() =>\n Promise.resolve(\n writes.insert(tables.drafts).values({\n id,\n values: item.values,\n model: null,\n form: '',\n updatedAt: at.toISOString(),\n updatedBy: nameOf(item.as),\n baseRevision: null,\n }),\n ),\n )\n const facts: Facts = {\n archivedAt: null,\n row: 'none',\n draft: { scheduledFor: null, scheduleError: null },\n }\n const published = yield* publishing({ entry, served, facts }, item.as, null, at)\n return { entry: id, targetId: published.output.targetId }\n }),\n )\n\n const Unpublish = operation(Cms.Operations.Unpublish, ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry, served } = yield* offering(input.entry, 'unpublish')\n yield* asking(principal, 'unpublish', entry)\n const shown = served.type.roles.published!\n const writes = yield* drizzleWrites\n yield* Effect.promise(() =>\n Promise.resolve(\n writes\n .update(served.binding.table)\n .set({ [shown.key]: null })\n .where(eq(served.binding.columns.id!, entry.targetId!))\n .returning({ id: served.binding.columns.id! }),\n ),\n )\n const content = returning(served.binding, [shown.key])\n return {\n output: {},\n entities: [\n ...content.patches(yield* readRows(served.binding, content.columns, entry.targetId!)),\n ...(yield* entryPatches(entry.id)),\n ],\n }\n }),\n )\n\n const sources: ReadonlyArray<EntitySource<P, DrizzleDatabase>> = [\n entries,\n source<P>(Db.Draft),\n source<P>(Db.Revision),\n ...config.content.map(served => source<P>(served.binding)),\n ]\n const mutations: ReadonlyArray<MutationSource<P, DrizzleDatabase>> = [\n SaveDraft,\n DiscardDraft,\n Publish,\n Unpublish,\n Schedule,\n Unschedule,\n archiving('archive'),\n archiving('unarchive'),\n Restore,\n ]\n\n // A content type with an address is found by it, behind the same boundary as\n // every other read of its table.\n // The descriptor carries what the query means, so this registers the source\n // and says no more: the address column and the order come from the body,\n // through the binding that knows which column holds which field.\n const bySlug = config.content.flatMap(({ type, binding }) =>\n type.roles.slug === undefined ? [] : [query(Cms.bySlug(type), { entity: binding })],\n )\n\n return {\n /**\n * The sources of the content types and of the CMS's own Entities, each behind\n * the audience boundary. Register these and not `source(binding)` of a content\n * table: that is how the boundary cannot be forgotten.\n */\n sources,\n queries: [worklist, ...bySlug] as ReadonlyArray<QuerySource<P, DrizzleDatabase>>,\n mutations,\n due,\n import: importing,\n /** The bindings of `Entry`, `Draft` and `Revision`, for a handler that returns patches of them. */\n bindings: Db,\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["text","integer","pgText","pgInteger"],"sources":["../src/tables.ts","../src/index.ts"],"sourcesContent":["/**\n * The three tables the CMS keeps beside the application's own. Drizzle tables\n * are made per dialect, so there is one definition for each; they have the same\n * columns. An application adds them to its schema and its migrations as it does\n * any table, and may define its own instead, so long as the columns are these.\n */\nimport { integer as pgInteger, jsonb, pgTable, text as pgText } from 'drizzle-orm/pg-core'\nimport { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\n/**\n * - `cms_entries`: a piece of content from first keystroke to archive. It exists\n * before its content row does.\n * - `cms_drafts`: an entry's one working copy; its id is the entry's.\n * - `cms_revisions`: published values, append-only.\n */\nexport const sqliteTables = () => ({\n entries: sqliteTable('cms_entries', {\n id: text('id').primaryKey(),\n type: text('type').notNull(),\n targetId: text('target_id'),\n label: text('label').notNull(),\n createdBy: text('created_by'),\n createdAt: text('created_at').notNull(),\n archivedAt: text('archived_at'),\n revision: integer('revision'),\n }),\n drafts: sqliteTable('cms_drafts', {\n id: text('id').primaryKey(),\n values: text('values', { mode: 'json' }),\n model: text('model', { mode: 'json' }),\n form: text('form').notNull(),\n updatedAt: text('updated_at').notNull(),\n updatedBy: text('updated_by'),\n baseRevision: integer('base_revision'),\n scheduledFor: text('scheduled_for'),\n scheduledBy: text('scheduled_by'),\n scheduleError: text('schedule_error'),\n }),\n revisions: sqliteTable('cms_revisions', {\n id: text('id').primaryKey(),\n entryId: text('entry_id').notNull(),\n n: integer('n').notNull(),\n values: text('values', { mode: 'json' }),\n publishedAt: text('published_at').notNull(),\n publishedBy: text('published_by'),\n }),\n})\n\nexport const pgTables = () => ({\n entries: pgTable('cms_entries', {\n id: pgText('id').primaryKey(),\n type: pgText('type').notNull(),\n targetId: pgText('target_id'),\n label: pgText('label').notNull(),\n createdBy: pgText('created_by'),\n createdAt: pgText('created_at').notNull(),\n archivedAt: pgText('archived_at'),\n revision: pgInteger('revision'),\n }),\n drafts: pgTable('cms_drafts', {\n id: pgText('id').primaryKey(),\n values: jsonb('values'),\n model: jsonb('model'),\n form: pgText('form').notNull(),\n updatedAt: pgText('updated_at').notNull(),\n updatedBy: pgText('updated_by'),\n baseRevision: pgInteger('base_revision'),\n scheduledFor: pgText('scheduled_for'),\n scheduledBy: pgText('scheduled_by'),\n scheduleError: pgText('schedule_error'),\n }),\n revisions: pgTable('cms_revisions', {\n id: pgText('id').primaryKey(),\n entryId: pgText('entry_id').notNull(),\n n: pgInteger('n').notNull(),\n values: jsonb('values'),\n publishedAt: pgText('published_at').notNull(),\n publishedBy: pgText('published_by'),\n }),\n})\n\n/** The `create table` statements for SQLite, for a test or a first migration. */\nexport const sqliteSchema = `\ncreate table if not exists cms_entries (\n id text primary key, type text not null, target_id text, label text not null,\n created_by text, created_at text not null, archived_at text, revision integer\n);\ncreate table if not exists cms_drafts (\n id text primary key, \"values\" text, model text, form text not null,\n updated_at text not null, updated_by text, base_revision integer,\n scheduled_for text, scheduled_by text, schedule_error text\n);\ncreate table if not exists cms_revisions (\n id text primary key, entry_id text not null, n integer not null, \"values\" text,\n published_at text not null, published_by text\n);\n`\n\nexport type CmsTables = ReturnType<typeof sqliteTables> | ReturnType<typeof pgTables>\n","/**\n * `foldkit-cms-drizzle` — the server half of `foldkit-cms`.\n *\n * It keeps an author's unpublished work in three tables beside the\n * application's own, and it is where the audience boundary is enforced, by a\n * `foldkit-guard` guard set: a principal its `authors` guards refuse is refused\n * entries, drafts and revisions outright, and sees of the content only the\n * rows the content's own Read rules show. Both are a binding's `visible`, so\n * they hold on every path a table is read by.\n *\n * Publishing runs the application's own mutation with the draft's value, inside\n * a transaction that also appends the revision and removes the draft, so a\n * publish happened entirely or did not happen.\n */\nimport {\n and,\n eq,\n inArray,\n isNotNull,\n isNull,\n lte,\n ne,\n sql,\n type AnyColumn,\n type SQL,\n} from 'drizzle-orm'\nimport { Effect, Exit, Option, Schema, Semaphore } from 'effect'\nimport { Cms, type Content, type Facts, type Transition } from 'foldkit-cms'\nimport { Entity, type AnyEntity } from 'foldkit-entity'\nimport { Guards, type Policy, type PrincipalGuard } from 'foldkit-guard'\nimport { GuardServer } from 'foldkit-guard/server'\nimport type { MutationDescriptor, RemoteForbidden } from 'foldkit-remote'\nimport {\n DrizzleDatabase,\n drizzleWrites,\n bind,\n query,\n returning,\n runStatement,\n source,\n type AnyEntityBinding,\n type DrizzleDatabaseService,\n type Visible,\n} from 'foldkit-remote-drizzle'\nimport {\n RemoteServer,\n MutationRefused,\n RemoteServerError,\n type EntitySource,\n type MutationOutcome,\n type MutationSource,\n type QuerySource,\n} from 'foldkit-remote-server'\nimport type { CmsTables } from './tables.js'\n\nexport { pgTables, sqliteSchema, sqliteTables, type CmsTables } from './tables.js'\n\n/**\n * A content type as the server holds it: its declaration, its Entity with the\n * guards attached that its binding was bound from, the binding of its table,\n * and the application's own handlers of its two publish mutations. What\n * publishing a post does stays the application's code.\n */\nexport interface ServedContent<P = any> {\n readonly type: Content<string, any, any, any>\n /** `type.entity` with the guard set's rules attached: `Post.pipe(Guards.attach(G, { … }))`. */\n readonly entity: AnyEntity\n readonly binding: AnyEntityBinding\n readonly create: MutationSource<P, DrizzleDatabase>\n readonly update: MutationSource<P, DrizzleDatabase>\n}\n\n/**\n * Runs some work so that it happened entirely or did not. Drizzle's own\n * transactions differ by driver, so the application says which this is. It\n * fails as the work does, or, when the database refuses the transaction\n * itself, as a `RemoteServerError` that says nothing of why.\n */\nexport type Transaction = <A, E>(\n work: Effect.Effect<A, E, DrizzleDatabase>,\n) => Effect.Effect<A, E | RemoteServerError, DrizzleDatabase>\n\nconst statement = (database: DrizzleDatabaseService, text: string) =>\n Effect.promise(() => {\n const runs = database as unknown as {\n run?: (query: SQL) => unknown\n execute?: (query: SQL) => unknown\n }\n return Promise.resolve(\n runs.run !== undefined ? runs.run(sql.raw(text)) : runs.execute!(sql.raw(text)),\n )\n })\n\n// One connection holds one transaction. Without this, a write another request\n// makes between this one's `begin` and its `rollback` is rolled back with it,\n// after it was reported done.\nconst turns = new WeakMap<object, Semaphore.Semaphore>()\nconst turnOf = (database: object): Semaphore.Semaphore => {\n const held = turns.get(database)\n if (held !== undefined) return held\n const made = Semaphore.makeUnsafe(1)\n turns.set(database, made)\n return made\n}\n\nexport const Transaction = {\n /**\n * `begin`, the work, then `commit` or `rollback`, as statements. For a database\n * that is one connection, such as a SQLite file, which it takes turns at: the\n * operations of this package wait for one another. A write of your own on the\n * same connection does not, so make it inside `Transaction.statements` too.\n * Over a pool each statement may take a different connection: use `drizzle` there.\n */\n statements: (<A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n return yield* turnOf(database).withPermit(\n statement(database, 'begin').pipe(\n Effect.andThen(work),\n Effect.onExit(exit => statement(database, Exit.isSuccess(exit) ? 'commit' : 'rollback')),\n ),\n )\n })) as Transaction,\n /**\n * Drizzle's `database.transaction`, with the work given the transaction as its\n * database. For a driver whose transactions are asynchronous: Postgres, libSQL.\n */\n drizzle: (<A, E>(work: Effect.Effect<A, E, DrizzleDatabase>) =>\n Effect.gen(function* () {\n const database = (yield* DrizzleDatabase) as unknown as {\n transaction: <T>(run: (inner: DrizzleDatabaseService) => Promise<T>) => Promise<T>\n }\n const failed = Symbol('failed')\n let exit: Exit.Exit<A, E> | undefined\n // A failure must leave the callback as a rejection, or Drizzle commits.\n yield* runStatement(\n () =>\n database\n .transaction(async inner => {\n exit = await Effect.runPromiseExit(\n work.pipe(Effect.provideService(DrizzleDatabase, inner)),\n )\n if (!Exit.isSuccess(exit)) throw failed\n })\n .catch(error => {\n if (error !== failed) throw error\n }),\n 'Database write failed',\n )\n return yield* exit!\n })) as Transaction,\n}\n\nexport interface CmsServerConfig<User> {\n readonly tables: CmsTables\n readonly content: ReadonlyArray<ServedContent<Option.Option<User>>>\n /** How a publish is made whole: `Transaction.statements` or `Transaction.drizzle`. */\n readonly transaction: Transaction\n /** The guard set's bodies, which decide `authors` and `transitions`. */\n readonly policy: Policy<User>\n /**\n * Who reads and writes unpublished work: a principal any of these lets.\n * Everyone else is a visitor.\n */\n readonly authors: ReadonlyArray<PrincipalGuard<User>>\n /**\n * Who of the authors may make a transition: any of its guards lets. One not\n * named is any author's.\n */\n readonly transitions?:\n { readonly [T in Transition]?: ReadonlyArray<PrincipalGuard<User>> | undefined } | undefined\n /**\n * Whether this author may make this transition of this entry, for a rule\n * about the entry itself: asked after the entry is found, the transition is\n * one its state offers, and `transitions` let. Default: they may.\n */\n readonly allow?: (\n principal: Option.Option<User>,\n transition: Transition,\n entry: EntryRow,\n ) => boolean\n /** The server's clock, passed in so a test can hold it. */\n readonly now?: () => Date\n /**\n * The most a draft may hold, as the characters of its JSON: its values and its\n * form's Model together. An author is trusted with their own content, not with\n * the database's disk. Default: one million.\n */\n readonly maxDraftSize?: number\n /** Who a principal is, for `createdBy` and `updatedBy`. */\n readonly nameOf?: (principal: Option.Option<User>) => string | null\n}\n\n/** A row of `cms_entries`, as the server's rules see it. */\nexport interface EntryRow {\n readonly id: string\n readonly type: string\n readonly targetId: string | null\n readonly label: string\n readonly archivedAt: string | null\n readonly revision: number | null\n}\n\n/**\n * One import: content published already, such as a site's first pages or what\n * another CMS held. `as` is whom it is imported as (an editor, so the create\n * handler writes the row); a runner that imports many fixes `as` once and\n * varies the rest. `at` is when it was published (default now); `entry`\n * names the entry (default a new id) — name it where another import points\n * at it, and import what is pointed at first.\n */\nexport interface ImportItem<P> {\n readonly type: string\n readonly values: unknown\n readonly as: P\n readonly at?: Date | undefined\n readonly entry?: string | undefined\n}\n\nconst refuse = (message: string) => new RemoteServerError({ message })\n\n/**\n * Whether a unique index on this column refused a write. Drivers say so in their\n * own words, and Drizzle wraps what they say, so every message down the chain of\n * causes is read. SQLite names the column; Postgres names the constraint, which\n * by default is made of the column's name.\n */\nconst refusedAsDuplicate = (defect: unknown, column: string): boolean => {\n const said: Array<string> = []\n for (let at = defect, depth = 0; at != null && depth < 5; depth++) {\n const { message, constraint, cause } = at as {\n readonly message?: unknown\n readonly constraint?: unknown\n readonly cause?: unknown\n }\n // Drizzle's own message quotes the statement, which names every column.\n if (!String(message).startsWith('Failed query'))\n said.push(String(message ?? at), String(constraint ?? ''))\n at = cause\n }\n const text = said.join(' ')\n const named = new RegExp(`(^|[^a-zA-Z0-9])${column.replace(/[^\\w]/g, '.')}([^a-zA-Z0-9]|$)`)\n return /unique|duplicate/i.test(text) && named.test(text)\n}\n\nexport const CmsServer = {\n make: <User>(config: CmsServerConfig<User>) => {\n type P = Option.Option<User>\n const { tables, policy } = config\n const now = config.now ?? (() => new Date())\n const nameOf = config.nameOf ?? (() => null)\n const maxDraftSize = config.maxDraftSize ?? 1_000_000\n\n const byType = new Map(config.content.map(served => [served.type.name, served]))\n for (const { type, entity, create, update } of config.content) {\n if (!Entity.same(entity, type.entity))\n throw new Error(\n `foldkit-cms-drizzle: content \"${type.name}\" is of ${type.entity.name}, but was given ${entity.name}'s guards`,\n )\n for (const [handler, declared] of [\n [create, type.publish.create],\n [update, type.publish.update],\n ] as const)\n if (handler.mutation !== declared.name)\n throw new Error(\n `foldkit-cms-drizzle: content \"${type.name}\" publishes through \"${declared.name}\", but was given the handler of \"${handler.mutation}\"`,\n )\n const shown = type.roles.published\n if (shown === undefined) continue\n // Whether the rule is right is the application's to say. That it reads the\n // published field at all is checked here: one that does not shows a visitor\n // every row or none.\n if (!Guards.guardFields(entity, 'Read').includes(shown.key))\n throw new Error(\n `foldkit-cms-drizzle: content \"${type.name}\" can be unpublished, but its Read rule does not read \"${shown.key}\"; attach one that does, such as \\`Read: [G.isPublished, G.isAuthor]\\``,\n )\n }\n\n /** Whether any of `guards` lets `principal`, each body told `interaction`. */\n const anyOf = (\n guards: ReadonlyArray<PrincipalGuard<User>>,\n principal: P,\n interaction: string,\n ): Effect.Effect<boolean> =>\n Effect.map(\n Effect.forEach(guards, guard => policy.decide(guard, { principal, interaction })),\n decisions => decisions.some(decision => decision === true),\n )\n /**\n * Whether `principal` is an author, asked as the interaction it is for:\n * reading unpublished work, or changing it, so a body can tell the two.\n */\n const isAuthor = (principal: P, interaction: 'Read' | 'Update') =>\n anyOf(config.authors, principal, interaction)\n /** Whether `transitions` let this author make `transition`; an author's when it names none. */\n const transitionLets = (principal: P, transition: Transition) =>\n Option.match(Option.fromUndefinedOr(config.transitions?.[transition]), {\n onNone: () => Effect.succeed(true),\n onSome: guards => anyOf(guards, principal, transition),\n })\n\n // Unpublished work is an author's. To anyone else these tables are empty, on\n // every path they are read by.\n // The principal reaches a binding untyped: one that is not the guard set's is a\n // defect, never read as a visitor.\n const authorsOnly: Visible = principal =>\n policy.isPrincipal(principal)\n ? Effect.map(isAuthor(principal, 'Read'), author => (author ? undefined : sql`0 = 1`))\n : Effect.die(\n new Error('[foldkit-cms-drizzle] the principal is not an Option of the guard set’s'),\n )\n const Db = bind(Cms.Entities, {\n Entry: {\n table: tables.entries,\n visible: authorsOnly,\n relations: {\n draft: { foreignKey: tables.drafts.id },\n revisions: {\n foreignKey: tables.revisions.entryId,\n orderBy: [{ column: tables.revisions.n, direction: 'desc' }],\n },\n },\n // Not a column: this server derives it, below, with its own clock.\n derived: { state: { supplied: true }, may: { supplied: true } },\n },\n Draft: { table: tables.drafts, visible: authorsOnly },\n Revision: { table: tables.revisions, visible: authorsOnly },\n })\n\n /** What is known of some entries, for their state: their drafts, and whether a visitor sees their rows. */\n const factsOf = (entries: ReadonlyArray<EntryRow>) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const ids = entries.map(entry => entry.id)\n const drafts =\n ids.length === 0\n ? []\n : yield* runStatement(\n () =>\n database\n .select({\n id: tables.drafts.id,\n scheduledFor: tables.drafts.scheduledFor,\n scheduleError: tables.drafts.scheduleError,\n })\n .from(tables.drafts)\n .where(inArray(tables.drafts.id, ids)),\n 'Database query failed',\n )\n const draftOf = new Map(drafts.map(draft => [String(draft.id), draft]))\n\n // Per content type, which target rows exist and which a visitor would see.\n const rows = new Map<string, 'visible' | 'hidden'>()\n for (const [name, { type, binding }] of byType) {\n const targets = entries.flatMap(entry =>\n entry.type === name && entry.targetId !== null ? [entry.targetId] : [],\n )\n if (targets.length === 0) continue\n const id = binding.columns.id!\n const publishedColumn =\n type.roles.published === undefined\n ? undefined\n : binding.columns[type.roles.published.key]\n const found = yield* runStatement(\n () =>\n database\n .select({\n id,\n ...(publishedColumn === undefined ? {} : { shown: publishedColumn }),\n })\n .from(binding.table)\n .where(inArray(id, targets)),\n 'Database query failed',\n )\n for (const row of found)\n rows.set(\n `${name}:${String(row.id)}`,\n publishedColumn === undefined || row.shown !== null ? 'visible' : 'hidden',\n )\n }\n\n return new Map(\n entries.map((entry): readonly [string, Facts] => {\n const draft = draftOf.get(entry.id)\n return [\n entry.id,\n {\n archivedAt: entry.archivedAt,\n row:\n entry.targetId === null\n ? 'none'\n : (rows.get(`${entry.type}:${entry.targetId}`) ?? 'none'),\n draft:\n draft === undefined\n ? null\n : {\n scheduledFor: (draft.scheduledFor as string | null) ?? null,\n scheduleError: (draft.scheduleError as string | null) ?? null,\n },\n },\n ]\n }),\n )\n })\n\n const entryColumns = {\n id: tables.entries.id,\n type: tables.entries.type,\n targetId: tables.entries.targetId,\n label: tables.entries.label,\n archivedAt: tables.entries.archivedAt,\n revision: tables.entries.revision,\n }\n const findEntry = (id: string) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const [row] = yield* runStatement(\n () =>\n database\n .select(entryColumns)\n .from(tables.entries)\n .where(eq(tables.entries.id, id))\n .limit(1),\n 'Database query failed',\n )\n return row as EntryRow | undefined\n })\n\n // An entry's `state` and `may` are not columns. The generated source reads the\n // rest; the state is derived from what is known of each entry, with this server's\n // clock, and `may` is what `allow` lets the reader ask of it.\n const generated = source<P>(Db.Entry)\n const entries: EntitySource<P, DrizzleDatabase> = {\n ...generated,\n read: context =>\n Effect.gen(function* () {\n const wantsState = context.fields.includes('state')\n const wantsMay = context.fields.includes('may')\n const fields = context.fields.filter(field => field !== 'state' && field !== 'may')\n const records = yield* generated.read({\n ...context,\n // Both need these of each entry, asked for or not; `allow` is given the row.\n fields:\n wantsState || wantsMay\n ? [\n ...new Set([\n ...fields,\n 'type',\n 'targetId',\n 'archivedAt',\n ...(wantsMay ? ['label', 'revision'] : []),\n ]),\n ]\n : fields,\n })\n if (!wantsState && !wantsMay) return records\n const rows = records.map((record): EntryRow => ({\n id: record.id,\n type: String(record.values.type),\n targetId: (record.values.targetId as string | null) ?? null,\n label: String(record.values.label ?? ''),\n archivedAt: (record.values.archivedAt as string | null) ?? null,\n revision: (record.values.revision as number | null) ?? null,\n }))\n const facts = yield* factsOf(rows)\n const at = now()\n // Each transition's guards are decided once for this reader, not per entry.\n const lets = new Map(\n wantsMay\n ? yield* Effect.forEach(Cms.transitions, transition =>\n Effect.map(\n transitionLets(context.principal, transition),\n allowed => [transition, allowed] as const,\n ),\n )\n : [],\n )\n return records.map((record, index) => {\n const row = rows[index]!\n const known = facts.get(record.id)!\n return {\n id: record.id,\n values: {\n ...Object.fromEntries(\n Object.entries(record.values).filter(\n ([field]) => field === 'id' || context.fields.includes(field),\n ),\n ),\n ...(wantsState ? { state: Cms.state(known, at) } : {}),\n // What `transitions` and `allow` let this reader ask of this entry,\n // whatever its state offers now: the state says that, and a draft\n // saved a moment later changes it without asking the server again.\n ...(wantsMay\n ? {\n may: Cms.transitions.filter(\n transition =>\n lets.get(transition) === true &&\n config.allow?.(context.principal, transition, row) !== false,\n ),\n }\n : {}),\n },\n }\n })\n }),\n }\n\n // The descriptor carries all three questions and the order; this says only\n // which table answers them. The audience boundary is unchanged: `visible`\n // on the binding is conjoined with the body, as for every source here.\n const worklist: QuerySource<P, DrizzleDatabase> = query(Cms.Entries, { entity: Db.Entry })\n\n /** A mutation of this server: its principal and its database are fixed, its input is the descriptor's. */\n const operation = <Name extends string, Input, Output>(\n descriptor: MutationDescriptor<Name, Input, Output>,\n run: (context: {\n readonly input: Input\n readonly principal: P\n }) => Effect.Effect<\n MutationOutcome<Output>,\n RemoteServerError | MutationRefused | RemoteForbidden,\n DrizzleDatabase\n >,\n ): MutationSource<P, DrizzleDatabase> =>\n // Asked before anything is looked up: a visitor is not told which entries there are.\n // Then the whole of it is one transaction: a save that made an entry and\n // could not make its draft leaves neither.\n RemoteServer.mutation<P, DrizzleDatabase, Name, Input, Output>(descriptor, context =>\n Effect.flatMap(isAuthor(context.principal, 'Update'), author =>\n author\n ? config.transaction(run(context))\n : Effect.fail(RemoteServer.forbid('Only an author may change unpublished work')),\n ),\n )\n\n /**\n * A scheduled draft is published, when its time comes, as whoever scheduled it.\n * So changing it, replacing it or removing it is for someone who could have\n * scheduled it: otherwise an author who may not publish would publish through\n * someone who may.\n */\n const promised = (principal: P, entry: EntryRow) =>\n Effect.gen(function* () {\n const [draft] = yield* readRows(\n Db.Draft,\n { scheduledFor: tables.drafts.scheduledFor },\n entry.id,\n )\n if (draft?.scheduledFor == null) return\n if (\n !(yield* transitionLets(principal, 'schedule')) ||\n config.allow?.(principal, 'schedule', entry) === false\n )\n return yield* RemoteServer.forbid(\n 'This draft is scheduled, and only someone who may schedule it may change it',\n )\n })\n\n /**\n * Whether this author may make this transition: its `transitions`, and `allow`\n * of the entry once there is one.\n */\n const asking = (principal: P, transition: Transition, entry: EntryRow | undefined) =>\n Effect.flatMap(transitionLets(principal, transition), lets =>\n !lets || (entry !== undefined && config.allow?.(principal, transition, entry) === false)\n ? Effect.fail(RemoteServer.forbid(`This author may not ${transition} this entry`))\n : Effect.void,\n )\n\n const draftFields = ['values', 'model', 'form', 'updatedAt', 'updatedBy', 'baseRevision']\n const draftPatch = returning(Db.Draft, draftFields)\n const entryPatch = returning(Db.Entry, [\n 'type',\n 'targetId',\n 'label',\n 'createdAt',\n 'archivedAt',\n 'revision',\n ])\n /**\n * The entry as the client should now hold it, its state with it: an operation\n * changes the state, and a patch without it would leave the old one on screen.\n */\n const entryPatches = (id: string) =>\n Effect.gen(function* () {\n const patches = entryPatch.patches(yield* readRows(Db.Entry, entryPatch.columns, id))\n const entry = yield* findEntry(id)\n if (entry === undefined) return patches\n const facts = (yield* factsOf([entry])).get(id)!\n return patches.map(patch => ({\n ...patch,\n values: { ...patch.values, state: Cms.state(facts, now()) },\n }))\n })\n const readRows = (binding: AnyEntityBinding, columns: Record<string, AnyColumn>, id: string) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n return yield* runStatement(\n () => database.select(columns).from(binding.table).where(eq(binding.columns.id!, id)),\n 'Database query failed',\n )\n })\n\n const SaveDraft = operation(Cms.Operations.SaveDraft, ({ input, principal }) =>\n Effect.gen(function* () {\n if (!byType.has(input.type))\n return yield* refuse(`\"${input.type}\" is not a type of content this server knows`)\n const size = JSON.stringify([input.values, input.model]).length\n if (size > maxDraftSize)\n return yield* refuse(\n `This draft is too large to save: ${size} characters, and ${maxDraftSize} is the most`,\n )\n // An id nobody has is something new: the client named it, and this makes it.\n const existing = yield* findEntry(input.entry)\n if (existing !== undefined && existing.type !== input.type)\n return yield* refuse(`This entry is of \"${existing.type}\", not \"${input.type}\"`)\n if (existing?.archivedAt != null) return yield* refuse('An archived entry takes no draft')\n yield* asking(principal, 'save', existing)\n if (existing !== undefined) yield* promised(principal, existing)\n\n const database = yield* drizzleWrites\n const who = nameOf(principal)\n const id = existing?.id ?? input.entry\n const [held] = yield* readRows(Db.Draft, { updatedAt: tables.drafts.updatedAt }, id)\n const previous = held === undefined ? null : String(held.updatedAt)\n // Two saves in one millisecond must still be told apart by the next one.\n const at = now().toISOString()\n const updatedAt =\n previous !== null && at <= previous\n ? new Date(new Date(previous).getTime() + 1).toISOString()\n : at\n const written = {\n values: input.values,\n model: input.model,\n form: input.form,\n updatedAt,\n updatedBy: who,\n // A draft that changed is worth trying again when its time comes.\n scheduleError: null,\n }\n\n // The first draft of an entry has nothing to have been based on. Asked before\n // anything is written, so a refusal leaves no entry behind.\n if (previous === null && input.basedOn !== null)\n return yield* RemoteServer.refuse(\n Cms.Operations.SaveDraft,\n Cms.refusal.conflict,\n 'This draft was discarded',\n )\n if (existing === undefined) {\n yield* runStatement(\n () =>\n database.insert(tables.entries).values({\n id,\n type: input.type,\n targetId: null,\n label: input.label,\n createdBy: who,\n createdAt: at,\n archivedAt: null,\n revision: null,\n }),\n 'Database write failed',\n )\n }\n if (previous === null) {\n yield* runStatement(\n () => database.insert(tables.drafts).values({ id, ...written, baseRevision: null }),\n 'Database write failed',\n )\n } else {\n // Compare and set: the save names what it was made from, and a newer one\n // on the server means someone else saved in between.\n const changed = yield* runStatement(\n () =>\n database\n .update(tables.drafts)\n .set(written)\n .where(\n and(eq(tables.drafts.id, id), eq(tables.drafts.updatedAt, input.basedOn ?? '')),\n )\n .returning({ id: tables.drafts.id }),\n 'Database write failed',\n )\n if (changed.length === 0)\n return yield* RemoteServer.refuse(\n Cms.Operations.SaveDraft,\n Cms.refusal.conflict,\n 'This draft was saved by someone else since',\n )\n }\n if (existing !== undefined && existing.label !== input.label) {\n yield* runStatement(\n () =>\n database\n .update(tables.entries)\n .set({ label: input.label })\n .where(eq(tables.entries.id, id))\n .returning({ id: tables.entries.id }),\n 'Database write failed',\n )\n }\n\n const entities = [\n ...(yield* entryPatches(id)),\n ...draftPatch.patches(yield* readRows(Db.Draft, draftPatch.columns, id)),\n ]\n return { output: { entry: id as never, updatedAt }, entities }\n }),\n )\n\n const DiscardDraft = operation(Cms.Operations.DiscardDraft, ({ input, principal }) =>\n Effect.gen(function* () {\n const entry = yield* findEntry(input.entry)\n if (entry === undefined) return yield* refuse('There is no such entry')\n yield* asking(principal, 'discard', entry)\n yield* promised(principal, entry)\n const database = yield* drizzleWrites\n yield* runStatement(\n () => database.delete(tables.drafts).where(eq(tables.drafts.id, entry.id)),\n 'Database write failed',\n )\n const gone = [{ entity: 'CmsDraft', id: entry.id }]\n // Never published, and now with nothing entered: there is nothing left of it.\n if (entry.targetId !== null)\n return { output: {}, deleted: gone, entities: yield* entryPatches(entry.id) }\n yield* runStatement(\n () => database.delete(tables.entries).where(eq(tables.entries.id, entry.id)),\n 'Database write failed',\n )\n return { output: {}, deleted: [...gone, { entity: 'CmsEntry', id: entry.id }] }\n }),\n )\n\n const done: Readonly<Record<Exclude<Transition, 'save' | 'discard'>, string>> = {\n publish: 'published',\n unpublish: 'unpublished',\n schedule: 'scheduled',\n unschedule: 'unscheduled',\n archive: 'archived',\n unarchive: 'unarchived',\n restore: 'restored',\n }\n /** The entry, if its state offers this transition now. */\n const offering = (id: string, transition: Exclude<Transition, 'save' | 'discard'>) =>\n Effect.gen(function* () {\n const entry = yield* findEntry(id)\n if (entry === undefined) return yield* refuse('There is no such entry')\n const served = byType.get(entry.type)\n if (served === undefined)\n return yield* refuse(`\"${entry.type}\" is not a type of content this server knows`)\n const facts = (yield* factsOf([entry])).get(entry.id)!\n if (!Cms.offers(facts, now(), served.type).includes(transition))\n return yield* refuse(`This entry cannot be ${done[transition]} as it stands`)\n return { entry, served, facts }\n })\n\n /** What the `published` role's column holds for \"now\". */\n const stamp = (column: AnyColumn, at: Date) => {\n const kind = column.dataType.split(' ')[0]\n return kind === 'string' ? at.toISOString() : kind === 'number' ? at.getTime() : at\n }\n\n const scheduledPatch = returning(Db.Draft, ['scheduledFor', 'scheduleError'])\n const revisionPatch = returning(Db.Revision, ['n', 'values', 'publishedAt', 'publishedBy'])\n\n type Found = Effect.Success<ReturnType<typeof offering>>\n\n /** The draft as the publish mutation would take it, or why it would not. */\n const readied = ({ entry, served, facts }: Found) =>\n Effect.gen(function* () {\n const [draft] = yield* readRows(Db.Draft, { values: tables.drafts.values }, entry.id)\n // A row that was deleted under its entry is made again.\n const creating = facts.row === 'none'\n const handler = creating ? served.create : served.update\n const value =\n creating || typeof draft?.values !== 'object' || draft.values === null\n ? draft?.values\n : { ...draft.values, id: entry.targetId }\n const decoded = yield* Schema.decodeUnknownEffect(handler.Input)(value).pipe(\n Effect.mapError(error => refuse(`This draft is not ready to publish: ${String(error)}`)),\n )\n return { draft, creating, handler, decoded }\n })\n\n /** A publish, for the operation that asks for one, the schedule that comes due, and an import. */\n const publishing = (found: Found, principal: P, basedOn: number | null, at: Date = now()) =>\n Effect.gen(function* () {\n const { entry, served } = found\n const database = yield* DrizzleDatabase\n const held = entry.revision\n const conflict = RemoteServer.refuse(\n Cms.Operations.Publish,\n Cms.refusal.conflict,\n 'This entry was published by someone else since',\n )\n if (held !== basedOn) return yield* conflict\n const { draft, creating, handler, decoded } = yield* readied(found)\n\n // A taken slug is refused by name, on its key. This is advice: two publishes\n // can both pass it, and then the unique index is the rule, below.\n const address = served.type.roles.slug\n const slug =\n address === undefined\n ? undefined\n : (decoded as Readonly<Record<string, unknown>> | null)?.[address.key]\n const taken = (key: string) =>\n RemoteServer.refuse(\n Cms.Operations.Publish,\n Cms.refusal.field(key, `That address is taken: \"${String(slug)}\" is already used`),\n )\n if (address !== undefined && typeof slug === 'string') {\n const column = served.binding.columns[address.key]!\n const id = served.binding.columns.id!\n const others = yield* runStatement(\n () =>\n database\n .select({ id })\n .from(served.binding.table)\n .where(and(eq(column, slug), creating ? undefined : ne(id, entry.targetId!)))\n .limit(1),\n 'Database query failed',\n )\n if (others.length > 0) return yield* taken(address.key)\n }\n\n return yield* Effect.gen(function* () {\n const writes = yield* drizzleWrites\n const n = (held ?? 0) + 1\n // Compare and set, first: of two publishes made from one revision, one\n // finds the number already moved, and nothing of it is written.\n const moved = yield* runStatement(\n () =>\n writes\n .update(tables.entries)\n .set({ revision: n })\n .where(\n and(\n eq(tables.entries.id, entry.id),\n held === null\n ? isNull(tables.entries.revision)\n : eq(tables.entries.revision, held),\n ),\n )\n .returning({ id: tables.entries.id }),\n 'Database write failed',\n )\n if (moved.length === 0) return yield* conflict\n // Scheduled publishing has no client request, so the id names this\n // publish of this entry: the same on a retry, another on the next.\n const ran = yield* handler.run({\n input: decoded,\n principal,\n requestId: `publish:${entry.id}:${held ?? 'new'}`,\n })\n const targetId = creating\n ? String((ran.output as { readonly id: unknown }).id)\n : entry.targetId!\n const shown = served.type.roles.published\n if (shown !== undefined) {\n // Publishing shows the row. One already shown keeps its first date.\n const column = served.binding.columns[shown.key]!\n yield* runStatement(\n () =>\n writes\n .update(served.binding.table)\n .set({ [shown.key]: stamp(column, at) })\n .where(and(eq(served.binding.columns.id!, targetId), isNull(column)))\n .returning({ id: served.binding.columns.id! }),\n 'Database write failed',\n )\n }\n const revisionId = `${entry.id}:${n}`\n yield* runStatement(\n () =>\n writes.insert(tables.revisions).values({\n id: revisionId,\n entryId: entry.id,\n n,\n values: draft?.values,\n publishedAt: at.toISOString(),\n publishedBy: nameOf(principal),\n }),\n 'Database write failed',\n )\n yield* runStatement(\n () => writes.delete(tables.drafts).where(eq(tables.drafts.id, entry.id)),\n 'Database write failed',\n )\n yield* runStatement(\n () =>\n writes\n .update(tables.entries)\n .set({ targetId })\n .where(eq(tables.entries.id, entry.id))\n .returning({ id: tables.entries.id }),\n 'Database write failed',\n )\n return {\n output: { entry: entry.id as never, targetId, revision: n },\n entities: [\n ...ran.entities,\n // The row as the handler left it, every column: a handler need not patch what it wrote.\n ...(yield* returning.row(served.binding, targetId)),\n ...(yield* entryPatches(entry.id)),\n ...revisionPatch.patches(\n yield* readRows(Db.Revision, revisionPatch.columns, revisionId),\n ),\n ],\n connections: ran.connections,\n deleted: [...ran.deleted, { entity: 'CmsDraft', id: entry.id }],\n }\n }).pipe(\n // The publish that lost the race: the index refused it, and it is the same\n // news as the check's. Any other defect is left as it was.\n Effect.catchDefect(defect =>\n address !== undefined &&\n refusedAsDuplicate(defect, served.binding.columns[address.key]!.name)\n ? Effect.fail(taken(address.key))\n : Effect.die(defect),\n ),\n )\n })\n\n const Publish = operation(Cms.Operations.Publish, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'publish')\n yield* asking(principal, 'publish', found.entry)\n return yield* publishing(found, principal, input.basedOn)\n }),\n )\n\n /** Writes to an entry's draft, and answers with both as the client should now hold them. */\n const drafted = (id: string, values: object) =>\n Effect.gen(function* () {\n const writes = yield* drizzleWrites\n yield* runStatement(\n () =>\n writes\n .update(tables.drafts)\n .set(values)\n .where(eq(tables.drafts.id, id))\n .returning({ id: tables.drafts.id }),\n 'Database write failed',\n )\n return {\n output: {},\n entities: [\n ...(yield* entryPatches(id)),\n ...scheduledPatch.patches(yield* readRows(Db.Draft, scheduledPatch.columns, id)),\n ],\n }\n })\n\n const Schedule = operation(Cms.Operations.Schedule, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'schedule')\n yield* asking(principal, 'schedule', found.entry)\n if (Number.isNaN(new Date(input.at).getTime()))\n return yield* refuse(`\"${input.at}\" is not a time`)\n // What cannot be published now is not promised for later.\n yield* readied(found)\n return yield* drafted(found.entry.id, {\n scheduledFor: new Date(input.at).toISOString(),\n scheduledBy: nameOf(principal),\n scheduleError: null,\n })\n }),\n )\n\n const Unschedule = operation(Cms.Operations.Unschedule, ({ input, principal }) =>\n Effect.gen(function* () {\n const found = yield* offering(input.entry, 'unschedule')\n yield* asking(principal, 'unschedule', found.entry)\n return yield* drafted(found.entry.id, {\n scheduledFor: null,\n scheduledBy: null,\n scheduleError: null,\n })\n }),\n )\n\n /** Archiving and its undoing: a fact about the entry, and for what can be hidden, the row. */\n const archiving = (transition: 'archive' | 'unarchive') =>\n operation(\n transition === 'archive' ? Cms.Operations.Archive : Cms.Operations.Unarchive,\n ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry, served } = yield* offering(input.entry, transition)\n yield* asking(principal, transition, entry)\n const shown = served.type.roles.published\n // Archiving what is on show takes it off: that is unpublishing, and asks as it does.\n if (transition === 'archive' && shown !== undefined && entry.targetId !== null) {\n const published = returning(served.binding, [shown.key])\n const [row] = published.patches(\n yield* readRows(served.binding, published.columns, entry.targetId),\n )\n const onShow =\n row !== undefined && Option.isSome(Option.fromNullishOr(row.values[shown.key]))\n if (onShow) yield* asking(principal, 'unpublish', entry)\n }\n const writes = yield* drizzleWrites\n yield* runStatement(\n () =>\n writes\n .update(tables.entries)\n .set({ archivedAt: transition === 'archive' ? now().toISOString() : null })\n .where(eq(tables.entries.id, entry.id))\n .returning({ id: tables.entries.id }),\n 'Database write failed',\n )\n if (transition === 'unarchive' || shown === undefined || entry.targetId === null)\n return { output: {}, entities: yield* entryPatches(entry.id) }\n // What is put away is not left on show. It comes back as unpublished work.\n yield* runStatement(\n () =>\n writes\n .update(served.binding.table)\n .set({ [shown.key]: null })\n .where(eq(served.binding.columns.id!, entry.targetId!))\n .returning({ id: served.binding.columns.id! }),\n 'Database write failed',\n )\n const content = returning(served.binding, [shown.key])\n return {\n output: {},\n entities: [\n ...content.patches(\n yield* readRows(served.binding, content.columns, entry.targetId),\n ),\n ...(yield* entryPatches(entry.id)),\n ],\n }\n }),\n )\n\n /**\n * A revision's value becomes the working copy, and nothing is published. It\n * replaces what the draft held, and takes back a promise made of that: what\n * was scheduled is not what is there now. The saved Model goes too, so an\n * editor fills its form from the values, key by key.\n */\n const Restore = operation(Cms.Operations.Restore, ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry } = yield* offering(input.entry, 'restore')\n yield* asking(principal, 'restore', entry)\n yield* promised(principal, entry)\n const database = yield* DrizzleDatabase\n const [revision] = yield* runStatement(\n () =>\n database\n .select({ values: tables.revisions.values })\n .from(tables.revisions)\n .where(\n and(eq(tables.revisions.entryId, entry.id), eq(tables.revisions.n, input.revision)),\n )\n .limit(1),\n 'Database query failed',\n )\n if (revision === undefined)\n return yield* refuse(`This entry has no revision ${input.revision}`)\n\n const writes = yield* drizzleWrites\n const [held] = yield* readRows(Db.Draft, { updatedAt: tables.drafts.updatedAt }, entry.id)\n const at = now().toISOString()\n const updatedAt =\n held !== undefined && at <= String(held.updatedAt)\n ? new Date(new Date(String(held.updatedAt)).getTime() + 1).toISOString()\n : at\n const restored = {\n values: revision.values,\n model: null,\n form: `restored@${input.revision}`,\n updatedAt,\n updatedBy: nameOf(principal),\n baseRevision: entry.revision,\n scheduledFor: null,\n scheduledBy: null,\n scheduleError: null,\n }\n yield* runStatement(\n () =>\n held === undefined\n ? writes.insert(tables.drafts).values({ id: entry.id, ...restored })\n : writes\n .update(tables.drafts)\n .set(restored)\n .where(eq(tables.drafts.id, entry.id))\n .returning({ id: tables.drafts.id }),\n 'Database write failed',\n )\n return {\n output: { updatedAt },\n entities: [\n ...(yield* entryPatches(entry.id)),\n ...draftPatch.patches(yield* readRows(Db.Draft, draftPatch.columns, entry.id)),\n ...scheduledPatch.patches(yield* readRows(Db.Draft, scheduledPatch.columns, entry.id)),\n ],\n }\n }),\n )\n\n /**\n * Publishes every draft whose time has come, each in its own transaction, as\n * whoever scheduled it: `as` says who a stored name is. One that fails stays\n * scheduled with the reason, and is left alone until its draft changes, so a\n * fault the author must fix is not tried again every minute. The package owns\n * no timer: a cron trigger, an interval or a queue calls this.\n */\n const due = (at: Date, options: { readonly as: (scheduledBy: string | null) => P }) =>\n Effect.gen(function* () {\n const database = yield* DrizzleDatabase\n const waiting = yield* runStatement(\n () =>\n database\n .select({ id: tables.drafts.id, scheduledBy: tables.drafts.scheduledBy })\n .from(tables.drafts)\n .where(\n and(\n isNotNull(tables.drafts.scheduledFor),\n lte(tables.drafts.scheduledFor, at.toISOString()),\n isNull(tables.drafts.scheduleError),\n // What is put away keeps its promise, and keeps it waiting.\n sql`${tables.drafts.id} in (select ${tables.entries.id} from ${tables.entries} where ${tables.entries.archivedAt} is null)`,\n ),\n ),\n 'Database query failed',\n )\n const outcomes: Array<{ readonly entry: string; readonly error: string | null }> = []\n for (const draft of waiting) {\n const id = String(draft.id)\n const principal = options.as((draft.scheduledBy as string | null) ?? null)\n const error = yield* config\n .transaction(\n Effect.gen(function* () {\n if (!(yield* isAuthor(principal, 'Update')))\n return yield* RemoteServer.forbid('Whoever scheduled this is no longer an author')\n const found = yield* offering(id, 'publish')\n yield* asking(principal, 'publish', found.entry)\n yield* publishing(found, principal, found.entry.revision)\n return null\n }),\n )\n .pipe(\n Effect.catch(failure => Effect.succeed(failure.message)),\n Effect.catchDefect(defect => Effect.succeed(String(defect))),\n )\n // Recorded after the failed publish was undone, or it would be undone with it.\n if (error !== null) yield* config.transaction(drafted(id, { scheduleError: error }))\n outcomes.push({ entry: id, error })\n }\n return outcomes\n })\n\n /**\n * Content published already, such as a site's first pages or what another CMS\n * held, written by the path a publish takes: an entry and a draft of\n * `values`, then the publish, which runs the type's own `create` handler as\n * `as` and records revision 1. It is the server's act, not an author's, so\n * `allow` is not asked. `at` is when it was published (default now); `entry`\n * names the entry (default a new id). One transaction: it all lands or none.\n */\n const importing = (item: ImportItem<P>) =>\n config.transaction(\n Effect.gen(function* () {\n const served = byType.get(item.type)\n if (served === undefined)\n return yield* refuse(`\"${item.type}\" is not a type of content this server knows`)\n const writes = yield* drizzleWrites\n const id = item.entry ?? Cms.newEntryId()\n const at = item.at ?? now()\n const labelKey = served.type.roles.label?.key\n const named =\n labelKey === undefined\n ? undefined\n : (item.values as Readonly<Record<string, unknown>> | null)?.[labelKey]\n const entry: EntryRow = {\n id,\n type: item.type,\n targetId: null,\n label: typeof named === 'string' ? named : '',\n archivedAt: null,\n revision: null,\n }\n yield* runStatement(\n () =>\n writes.insert(tables.entries).values({\n ...entry,\n createdBy: nameOf(item.as),\n createdAt: at.toISOString(),\n }),\n 'Database write failed',\n )\n yield* runStatement(\n () =>\n writes.insert(tables.drafts).values({\n id,\n values: item.values,\n model: null,\n form: '',\n updatedAt: at.toISOString(),\n updatedBy: nameOf(item.as),\n baseRevision: null,\n }),\n 'Database write failed',\n )\n const facts: Facts = {\n archivedAt: null,\n row: 'none',\n draft: { scheduledFor: null, scheduleError: null },\n }\n const published = yield* publishing({ entry, served, facts }, item.as, null, at)\n return { entry: id, targetId: published.output.targetId }\n }),\n )\n\n const Unpublish = operation(Cms.Operations.Unpublish, ({ input, principal }) =>\n Effect.gen(function* () {\n const { entry, served } = yield* offering(input.entry, 'unpublish')\n yield* asking(principal, 'unpublish', entry)\n const shown = served.type.roles.published!\n const writes = yield* drizzleWrites\n yield* runStatement(\n () =>\n writes\n .update(served.binding.table)\n .set({ [shown.key]: null })\n .where(eq(served.binding.columns.id!, entry.targetId!))\n .returning({ id: served.binding.columns.id! }),\n 'Database write failed',\n )\n const content = returning(served.binding, [shown.key])\n return {\n output: {},\n entities: [\n ...content.patches(yield* readRows(served.binding, content.columns, entry.targetId!)),\n ...(yield* entryPatches(entry.id)),\n ],\n }\n }),\n )\n\n const sources: ReadonlyArray<EntitySource<P, DrizzleDatabase>> = [\n entries,\n source<P>(Db.Draft),\n source<P>(Db.Revision),\n // A content type's own members' Read rules, as well as its rows'.\n ...config.content.map(served =>\n source<P>(served.binding, { filter: GuardServer.filter(served.entity, policy) }),\n ),\n ]\n const mutations: ReadonlyArray<MutationSource<P, DrizzleDatabase>> = [\n SaveDraft,\n DiscardDraft,\n Publish,\n Unpublish,\n Schedule,\n Unschedule,\n archiving('archive'),\n archiving('unarchive'),\n Restore,\n ]\n\n // A content type with an address is found by it, behind the same boundary as\n // every other read of its table.\n // The descriptor carries what the query means, so this registers the source\n // and says no more: the address column and the order come from the body,\n // through the binding that knows which column holds which field.\n const slugQueries = config.content.flatMap(({ type, binding }) =>\n type.roles.slug === undefined ? [] : [{ descriptor: Cms.bySlug(type), binding }],\n )\n const bySlug = slugQueries.map(({ descriptor, binding }) =>\n query(descriptor, { entity: binding }),\n )\n // The content's bindings must apply the guard set's own rules, not one that only\n // looks like them: what `GuardServer.validate` checks of any server.\n GuardServer.validate(policy, {\n entities: config.content.map(served => served.entity),\n domain: {\n registry: {\n queries: new Map(slugQueries.map(({ descriptor }) => [descriptor.name, descriptor])),\n },\n },\n server: RemoteServer.make({ entities: sources, queries: bySlug }),\n })\n\n return {\n /**\n * The sources of the content types and of the CMS's own Entities, each behind\n * the audience boundary. Register these and not `source(binding)` of a content\n * table: that is how the boundary cannot be forgotten.\n */\n sources,\n queries: [worklist, ...bySlug] as ReadonlyArray<QuerySource<P, DrizzleDatabase>>,\n mutations,\n due,\n import: importing,\n /** The bindings of `Entry`, `Draft` and `Revision`, for a handler that returns patches of them. */\n bindings: Db,\n }\n },\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-cms-drizzle",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "The server half of foldkit-cms over Drizzle: the entries, drafts and revisions tables, the draft operations with their conflict rule, the worklist query, and the audience boundary between a visitor and an author.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -39,11 +39,12 @@
39
39
  "LICENSE"
40
40
  ],
41
41
  "dependencies": {
42
- "foldkit-cms": "0.5.0",
43
- "foldkit-remote": "0.13.0",
44
- "foldkit-remote-drizzle": "0.10.1",
45
- "foldkit-remote-server": "0.12.1",
46
- "foldkit-entity": "0.7.0"
42
+ "foldkit-cms": "0.6.1",
43
+ "foldkit-guard": "0.1.1",
44
+ "foldkit-remote": "0.14.1",
45
+ "foldkit-entity": "0.8.1",
46
+ "foldkit-remote-drizzle": "0.11.1",
47
+ "foldkit-remote-server": "0.13.1"
47
48
  },
48
49
  "peerDependencies": {
49
50
  "drizzle-orm": "1.0.0-rc.4",
@@ -56,9 +57,9 @@
56
57
  "foldkit": "0.165.0",
57
58
  "tsdown": "^0.23.0",
58
59
  "typescript": "5.9.3",
59
- "foldkit-form": "0.4.1",
60
- "foldkit-bundle": "0.5.0",
61
- "foldkit-surface": "0.7.0"
60
+ "foldkit-form": "0.4.3",
61
+ "foldkit-surface": "0.8.0",
62
+ "foldkit-bundle": "0.6.0"
62
63
  },
63
64
  "scripts": {
64
65
  "build": "tsdown",