@sealcode/sealgen 0.12.13 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="koa__router" resolution-mode="require"/>
2
+ /// <reference types="koa__router" />
3
3
  import Router from "@koa/router";
4
4
  import { Context } from "koa";
5
5
  import { Field, FieldsetOutput } from "sealious";
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="koa__router" resolution-mode="require"/>
3
+ /// <reference types="koa__router" />
4
4
  import Router from "@koa/router";
5
5
  import { Context } from "koa";
6
6
  import { ParsedUrlQuery } from "querystring";
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { Context } from "koa";
4
4
  import { FlatTemplatable } from "tempstream";
@@ -1,7 +1,7 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  /// <reference types="node" resolution-mode="require"/>
3
3
  import Router from "@koa/router";
4
- import Sealious from "sealious";
4
+ import { CollectionItem, File as SealiousFile, Field as SealiousField, Collection } from "sealious";
5
5
  import { Context } from "koa";
6
6
  import { FlatTemplatable } from "tempstream";
7
7
  import { MultipleFiles as MultipleFilesField } from "../fields/multiple-files.js";
@@ -10,7 +10,7 @@ import { FormControlContext } from "./form-control.js";
10
10
  export type MultipleFilesOptions = {
11
11
  label?: string;
12
12
  uploadLabel?: string;
13
- getAdditionalFields: (ctx: Context, file: Sealious.File) => Promise<Record<string, unknown>>;
13
+ getAdditionalFields: (ctx: Context, file: SealiousFile) => Promise<Record<string, unknown>>;
14
14
  };
15
15
  export declare class MultipleFiles extends FormFieldControl {
16
16
  field: MultipleFilesField;
@@ -19,18 +19,18 @@ export declare class MultipleFiles extends FormFieldControl {
19
19
  getClassModifiers(): string[];
20
20
  mount(router: Router): void;
21
21
  deleteFileAssociation(ctx: Context, file_item_id: string): Promise<void>;
22
- addFileAssociation(ctx: Context, file: Sealious.File): Promise<void>;
22
+ addFileAssociation(ctx: Context, file: SealiousFile): Promise<void>;
23
23
  getFrameRelativeURL(): string;
24
24
  getFrameID(): string;
25
25
  renderFrame(ctx: Context, content?: FlatTemplatable): import("stream").Readable;
26
26
  render(fctx: FormControlContext): FlatTemplatable | Promise<FlatTemplatable>;
27
- getReferencingCollection(): Sealious.Collection;
28
- getFileField(): Sealious.Field | null;
29
- extractFileFromItem(item: Sealious.CollectionItem): Promise<Sealious.File | null>;
30
- renderFileItemPreview(_fileItem: Sealious.CollectionItem, file: Sealious.File): Promise<FlatTemplatable>;
31
- renderFileItem(fileItem: Sealious.CollectionItem, file: Sealious.File): Promise<FlatTemplatable>;
32
- renderFileRemoveButton(fileItem: Sealious.CollectionItem): FlatTemplatable;
33
- getFileItems(ctx: Context): Promise<Sealious.CollectionItem<any>[]>;
27
+ getReferencingCollection(): Collection;
28
+ getFileField(): SealiousField | null;
29
+ extractFileFromItem(item: CollectionItem): Promise<SealiousFile | null>;
30
+ renderFileItemPreview(_fileItem: CollectionItem, file: SealiousFile): Promise<FlatTemplatable>;
31
+ renderFileItem(fileItem: CollectionItem, file: SealiousFile): Promise<FlatTemplatable>;
32
+ renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable;
33
+ getFileItems(ctx: Context): Promise<CollectionItem<any>[]>;
34
34
  getInputAttributes(): {
35
35
  type: string;
36
36
  name: string;
@@ -1,14 +1,14 @@
1
1
  import { Context } from "koa";
2
- import Sealious from "sealious";
2
+ import { File as SealiousFile } from "sealious";
3
3
  import { FormDataValue } from "../form-types.js";
4
4
  import { FieldParseResult, FormField } from "./field.js";
5
5
  export type FileContainer = {
6
- old: Sealious.File | null;
7
- new: Sealious.File | null;
6
+ old: SealiousFile | null;
7
+ new: SealiousFile | null;
8
8
  };
9
9
  export declare class File<Required extends boolean> extends FormField<Required, FileContainer> {
10
10
  parse(ctx: Context, raw_value: FormDataValue): Promise<FieldParseResult<FileContainer>>;
11
- getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<Sealious.File | undefined>;
11
+ getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>): Promise<SealiousFile | undefined>;
12
12
  getEmptyValue(): {
13
13
  old: null;
14
14
  new: null;
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import { Context } from "koa";
3
3
  import Router from "@koa/router";
4
4
  import { FlatTemplatable } from "tempstream";
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { Context } from "koa";
4
4
  import { FlatTemplatable } from "tempstream";
@@ -1,8 +1,8 @@
1
- import Sealious, { App } from "sealious";
1
+ import { App, Context as SealiousContext } from "sealious";
2
2
  import { FormDataValue } from "./forms/form-types.js";
3
3
  declare module "koa" {
4
4
  interface BaseContext {
5
- $context: Sealious.Context;
5
+ $context: SealiousContext;
6
6
  $app: App;
7
7
  $body: Record<string, FormDataValue>;
8
8
  }
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { Mountable } from "./page/mountable.js";
4
4
  export declare function mount(router: Router, url: string | {
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { Context } from "koa";
4
4
  export declare abstract class Mountable {
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { Context } from "koa";
4
4
  import { FlatTemplatable } from "tempstream";
@@ -1,4 +1,4 @@
1
- /// <reference types="koa__router" resolution-mode="require"/>
1
+ /// <reference types="koa__router" />
2
2
  import Router from "@koa/router";
3
3
  import { BaseContext } from "koa";
4
4
  import { Templatable } from "tempstream";
@@ -8,9 +8,21 @@ export type StatefulPageActionDescription<ActionName> = {
8
8
  action: ActionName;
9
9
  label?: string;
10
10
  } | ActionName;
11
- export declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, (state: State, inputs: Record<string, string>, ...args: unknown[]) => State>> extends Mountable {
11
+ export type StateAndMetadata<State, Actions> = {
12
+ state: State;
13
+ inputs: Record<string, string>;
14
+ action: keyof Actions;
15
+ action_args: string;
16
+ } | {
17
+ state: State;
18
+ inputs: Record<string, string>;
19
+ action: null;
20
+ action_args: null;
21
+ $: Record<string, unknown>;
22
+ };
23
+ export declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, (state: State, inputs: Record<string, string>, ...args: unknown[]) => State | Promise<State>>> extends Mountable {
12
24
  abstract actions: Actions;
13
- abstract getInitialState(): State;
25
+ abstract getInitialState(): State | Promise<State>;
14
26
  abstract render(ctx: BaseContext, state: State, inputs: Record<string, string>): Templatable | Promise<Templatable>;
15
27
  canAccess(): Promise<{
16
28
  readonly canAccess: true;
@@ -24,22 +36,13 @@ export declare abstract class StatefulPage<State extends Record<string, unknown>
24
36
  label: string;
25
37
  };
26
38
  makeActionURL<ActionName extends keyof Actions>(action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
27
- makeActionButton<ActionName extends keyof Actions>(state: State, action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
39
+ makeActionButton<ActionName extends keyof Actions>(_state: State, action_description: StatefulPageActionDescription<ActionName>, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
28
40
  makeActionCallback<ActionName extends keyof Actions>(action_description: {
29
41
  action: ActionName;
30
42
  label?: string;
31
43
  } | ActionName, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
32
44
  rerender(): string;
33
- extractState(ctx: BaseContext): {
34
- state: State;
35
- inputs: Record<string, string>;
36
- action: keyof Actions;
37
- action_args: string;
38
- } | {
39
- state: State;
40
- inputs: Record<string, string>;
41
- action: null;
42
- action_args: null;
43
- };
45
+ preprocessRequestBody(values: Record<string, unknown>): Promise<StateAndMetadata<State, Actions>>;
46
+ extractState(ctx: BaseContext): Promise<StateAndMetadata<State, Actions>>;
44
47
  mount(router: Router, path: string): void;
45
48
  }
@@ -1,4 +1,6 @@
1
- import Sealious from "sealious";
1
+ import {
2
+ File as SealiousFile
3
+ } from "sealious";
2
4
  import { inputWrapper } from "../../utils/input-wrapper.js";
3
5
  import { tempstream } from "tempstream";
4
6
  import { FormFieldControl } from "./form-field-control.js";
@@ -105,7 +107,7 @@ class MultipleFiles extends FormFieldControl {
105
107
  if (!sealious_field) {
106
108
  return null;
107
109
  }
108
- return await Sealious.File.fromID(
110
+ return await SealiousFile.fromID(
109
111
  item.collection.app,
110
112
  item.get(sealious_field.name).id
111
113
  );
@@ -1,6 +1,6 @@
1
1
  import { tempstream } from "tempstream";
2
2
  import { File } from "./file.js";
3
- import Sealious from "sealious";
3
+ import { File as SealiousFile } from "sealious";
4
4
  class Photo extends File {
5
5
  constructor(field, imageRouter, options) {
6
6
  super(field, options);
@@ -20,7 +20,7 @@ class Photo extends File {
20
20
  if (!id) {
21
21
  return empty_container;
22
22
  }
23
- const file = await Sealious.File.fromID(ctx.$app, id);
23
+ const file = await SealiousFile.fromID(ctx.$app, id);
24
24
  return tempstream`<div class="${class_name}">
25
25
  ${this.imageRouter.image({
26
26
  resolutions: [100],
@@ -4,19 +4,19 @@ import {
4
4
  is,
5
5
  predicates
6
6
  } from "@sealcode/ts-predicates";
7
- import Sealious from "sealious";
7
+ import { File as SealiousFile } from "sealious";
8
8
  import { FormField } from "./field.js";
9
9
  class File extends FormField {
10
10
  async parse(ctx, raw_value) {
11
11
  let result;
12
- if (raw_value instanceof Sealious.File || is(raw_value, predicates.object) && hasShape(
12
+ if (raw_value instanceof SealiousFile || is(raw_value, predicates.object) && hasShape(
13
13
  {
14
14
  filename: predicates.string,
15
15
  id: predicates.string
16
16
  },
17
17
  raw_value
18
18
  ) && raw_value.id) {
19
- const file = await Sealious.File.fromID(
19
+ const file = await SealiousFile.fromID(
20
20
  ctx.$app,
21
21
  raw_value.id
22
22
  );
@@ -35,12 +35,12 @@ class File extends FormField {
35
35
  const old_file = hasFieldOfType(raw_value, "old", predicates.object) && hasShape(
36
36
  { id: predicates.string, filename: predicates.string },
37
37
  raw_value.old
38
- ) && raw_value.old.id ? await Sealious.File.fromID(ctx.$app, raw_value.old.id) : null;
38
+ ) && raw_value.old.id ? await SealiousFile.fromID(ctx.$app, raw_value.old.id) : null;
39
39
  const new_file = hasFieldOfType(
40
40
  raw_value,
41
41
  "new",
42
42
  predicates.array(predicates.object)
43
- ) && raw_value.new[0] instanceof Sealious.File ? raw_value.new[0] : null;
43
+ ) && raw_value.new[0] instanceof SealiousFile ? raw_value.new[0] : null;
44
44
  result = {
45
45
  parsable: true,
46
46
  error: null,
package/dist/mount.js CHANGED
@@ -1,4 +1,4 @@
1
- import Sealious from "sealious";
1
+ import { Middlewares as SealiousMiddlewares } from "sealious";
2
2
  async function handleHtmlPromise(ctx, next) {
3
3
  await next();
4
4
  if (ctx.body instanceof Promise) {
@@ -31,11 +31,11 @@ function mount(router, url, mountable, use_dummy_app = false) {
31
31
  };
32
32
  await next();
33
33
  },
34
- Sealious.Middlewares.parseBody(),
34
+ SealiousMiddlewares.parseBody(),
35
35
  handleHtmlPromise
36
36
  ] : [
37
- Sealious.Middlewares.extractContext(),
38
- Sealious.Middlewares.parseBody(),
37
+ SealiousMiddlewares.extractContext(),
38
+ SealiousMiddlewares.parseBody(),
39
39
  handleHtmlPromise
40
40
  ];
41
41
  router.use(raw_url, ...args);
@@ -1,4 +1,4 @@
1
- import { predicates, hasShape, is } from "@sealcode/ts-predicates";
1
+ import { predicates, hasShape, is, hasField } from "@sealcode/ts-predicates";
2
2
  import deepmerge from "deepmerge";
3
3
  import { tempstream } from "tempstream";
4
4
  import { from_base64, to_base64 } from "../utils/base64.js";
@@ -43,16 +43,18 @@ class StatefulPage extends Mountable {
43
43
  }
44
44
  makeActionURL(action_description, ...args) {
45
45
  const { action } = this.extractActionAndLabel(action_description);
46
- return `./?action=${action}&args=${to_base64(JSON.stringify(args))}`;
46
+ return `./?action=${action}&action_args=${to_base64(
47
+ JSON.stringify(args)
48
+ )}`;
47
49
  }
48
- makeActionButton(state, action_description, ...args) {
50
+ makeActionButton(_state, action_description, ...args) {
49
51
  const { label } = this.extractActionAndLabel(action_description);
50
52
  return (
51
53
  /* HTML */
52
54
  `
53
55
  <input
54
56
  type="submit"
55
- value=${label}
57
+ value="${label}"
56
58
  formaction="${this.makeActionURL(action_description, ...args)}"
57
59
  />
58
60
  `
@@ -67,12 +69,15 @@ class StatefulPage extends Mountable {
67
69
  rerender() {
68
70
  return "this.closest('form').requestSubmit()";
69
71
  }
70
- extractState(ctx) {
72
+ async preprocessRequestBody(values) {
73
+ return values;
74
+ }
75
+ async extractState(ctx) {
71
76
  if (!hasShape(
72
77
  {
73
78
  action: predicates.maybe(predicates.string),
74
79
  state: predicates.string,
75
- args: predicates.maybe(predicates.string),
80
+ action_args: predicates.maybe(predicates.string),
76
81
  $: predicates.maybe(predicates.object)
77
82
  },
78
83
  ctx.$body
@@ -80,43 +85,52 @@ class StatefulPage extends Mountable {
80
85
  console.error("Wrong data: ", ctx.$body);
81
86
  throw new Error("wrong formdata shape");
82
87
  }
88
+ const prepared_body = await this.preprocessRequestBody(ctx.$body);
83
89
  const inputs = Object.fromEntries(
84
- Object.entries(ctx.$body).filter(
90
+ Object.entries(prepared_body).filter(
85
91
  ([key]) => !["action", "state", "args", "$"].includes(key)
86
92
  )
87
93
  );
88
- const original_state = JSON.parse(from_base64(ctx.$body.state));
89
- const modified_state = deepmerge(original_state, ctx.$body.$ || {}, {
90
- arrayMerge: (_target, source, _options) => {
91
- return source;
94
+ const original_state_string = prepared_body.state;
95
+ const original_state = JSON.parse(
96
+ typeof original_state_string == "string" ? from_base64(original_state_string) : "{}"
97
+ );
98
+ const modified_state = deepmerge(
99
+ original_state,
100
+ hasField("$", prepared_body) ? prepared_body.$ : {},
101
+ {
102
+ arrayMerge: (_target, source, _options) => {
103
+ return source;
104
+ }
92
105
  }
93
- });
94
- if (ctx.$body.action && ctx.$body.args) {
106
+ );
107
+ if (prepared_body.action && prepared_body.action_args) {
95
108
  return {
96
109
  state: modified_state,
97
- action: ctx.$body.action,
110
+ action: prepared_body.action,
98
111
  inputs,
99
- action_args: ctx.$body.args
112
+ action_args: prepared_body.action_args
100
113
  };
101
114
  } else {
102
115
  return {
103
116
  state: modified_state,
104
117
  action: null,
118
+ inputs,
105
119
  action_args: null,
106
- inputs
120
+ $: hasField("$", prepared_body) ? prepared_body.$ : {}
107
121
  };
108
122
  }
109
123
  }
110
124
  mount(router, path) {
111
- router.get(path, (ctx) => {
112
- ctx.body = this.render(ctx, this.getInitialState(), {});
125
+ router.get(path, async (ctx) => {
126
+ ctx.body = this.render(ctx, await this.getInitialState(), {});
113
127
  });
114
- router.post(path, (ctx) => {
115
- const { action, state, inputs, action_args } = this.extractState(ctx);
128
+ router.post(path, async (ctx) => {
129
+ const { action, state, inputs, action_args } = await this.extractState(ctx);
116
130
  if (action) {
117
131
  ctx.body = this.render(
118
132
  ctx,
119
- this.actions[action](
133
+ await this.actions[action](
120
134
  state,
121
135
  inputs,
122
136
  ...JSON.parse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.12.13",
3
+ "version": "0.14.0",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -71,6 +71,6 @@
71
71
  "peerDependencies": {
72
72
  "koa": "^2.13.0",
73
73
  "koa-responsive-image-router": "^0.2.6",
74
- "sealious": "^0.17.50"
74
+ "sealious": "^0.18.0"
75
75
  }
76
76
  }
@@ -1,5 +1,10 @@
1
1
  import Router from "@koa/router";
2
- import Sealious from "sealious";
2
+ import {
3
+ CollectionItem,
4
+ File as SealiousFile,
5
+ Field as SealiousField,
6
+ Collection,
7
+ } from "sealious";
3
8
  import { Context } from "koa";
4
9
  import { inputWrapper } from "../../utils/input-wrapper.js";
5
10
  import { FlatTemplatable, tempstream } from "tempstream";
@@ -14,7 +19,7 @@ export type MultipleFilesOptions = {
14
19
  uploadLabel?: string;
15
20
  getAdditionalFields: (
16
21
  ctx: Context,
17
- file: Sealious.File
22
+ file: SealiousFile
18
23
  ) => Promise<Record<string, unknown>>;
19
24
  };
20
25
 
@@ -52,7 +57,7 @@ export class MultipleFiles extends FormFieldControl {
52
57
  );
53
58
 
54
59
  router.post(this.getFrameRelativeURL() + "/add", async (ctx) => {
55
- let files = ctx.$body.files as Sealious.File | Sealious.File[];
60
+ let files = ctx.$body.files as SealiousFile | SealiousFile[];
56
61
  if (!files || !is(files, predicates.object)) {
57
62
  ctx.body = "Missing files";
58
63
  return;
@@ -64,7 +69,7 @@ export class MultipleFiles extends FormFieldControl {
64
69
  files.map((file) =>
65
70
  this.addFileAssociation(
66
71
  ctx,
67
- file as unknown as Sealious.File
72
+ file as unknown as SealiousFile
68
73
  )
69
74
  )
70
75
  );
@@ -81,7 +86,7 @@ export class MultipleFiles extends FormFieldControl {
81
86
  ].removeByID(ctx.$context, file_item_id);
82
87
  }
83
88
 
84
- async addFileAssociation(ctx: Context, file: Sealious.File) {
89
+ async addFileAssociation(ctx: Context, file: SealiousFile) {
85
90
  const file_field = this.getFileField();
86
91
  if (!file_field) {
87
92
  throw new Error("No file field in referencing collection");
@@ -130,7 +135,7 @@ export class MultipleFiles extends FormFieldControl {
130
135
  return result;
131
136
  }
132
137
 
133
- getFileField(): Sealious.Field | null {
138
+ getFileField(): SealiousField | null {
134
139
  for (const [_, field] of Object.entries(
135
140
  this.getReferencingCollection().fields
136
141
  )) {
@@ -142,28 +147,28 @@ export class MultipleFiles extends FormFieldControl {
142
147
  }
143
148
 
144
149
  async extractFileFromItem(
145
- item: Sealious.CollectionItem
146
- ): Promise<Sealious.File | null> {
150
+ item: CollectionItem
151
+ ): Promise<SealiousFile | null> {
147
152
  const sealious_field = this.getFileField();
148
153
  if (!sealious_field) {
149
154
  return null;
150
155
  }
151
- return await Sealious.File.fromID(
152
- (item.collection as Sealious.Collection).app,
156
+ return await SealiousFile.fromID(
157
+ (item.collection as Collection).app,
153
158
  item.get(sealious_field.name).id
154
159
  );
155
160
  }
156
161
 
157
162
  async renderFileItemPreview(
158
- _fileItem: Sealious.CollectionItem,
159
- file: Sealious.File
163
+ _fileItem: CollectionItem,
164
+ file: SealiousFile
160
165
  ): Promise<FlatTemplatable> {
161
166
  return file.filename || "no filename";
162
167
  }
163
168
 
164
169
  async renderFileItem(
165
- fileItem: Sealious.CollectionItem,
166
- file: Sealious.File
170
+ fileItem: CollectionItem,
171
+ file: SealiousFile
167
172
  ): Promise<FlatTemplatable> {
168
173
  return tempstream/* HTML */ `<li class="file-list-item">
169
174
  <a
@@ -177,7 +182,7 @@ export class MultipleFiles extends FormFieldControl {
177
182
  </li>`;
178
183
  }
179
184
 
180
- renderFileRemoveButton(fileItem: Sealious.CollectionItem): FlatTemplatable {
185
+ renderFileRemoveButton(fileItem: CollectionItem): FlatTemplatable {
181
186
  return /* HTML */ ` <form
182
187
  data-turbo-frame="${this.getFrameID()}"
183
188
  action="${this.getFrameRelativeURL()}/delete/${fileItem.id}"
@@ -221,10 +226,7 @@ export class MultipleFiles extends FormFieldControl {
221
226
  await this.extractFileFromItem(item),
222
227
  ])
223
228
  )
224
- ).filter(([_, f]) => f !== null) as [
225
- Sealious.CollectionItem,
226
- Sealious.File
227
- ][];
229
+ ).filter(([_, f]) => f !== null) as [CollectionItem, SealiousFile][];
228
230
 
229
231
  return inputWrapper(
230
232
  ["multiple-files", this.field.name, ...this.getClassModifiers()],
@@ -1,7 +1,7 @@
1
1
  import { Context } from "koa";
2
2
  import { FlatTemplatable, tempstream } from "tempstream";
3
3
  import { File } from "./file.js";
4
- import Sealious from "sealious";
4
+ import { File as SealiousFile } from "sealious";
5
5
  import { FormField } from "../fields/field.js";
6
6
  import { SimpleInputOptions } from "./simple-input.js";
7
7
  import { FormControlContext } from "./form-control.js";
@@ -34,7 +34,7 @@ export class Photo extends File {
34
34
  if (!id) {
35
35
  return empty_container;
36
36
  }
37
- const file = await Sealious.File.fromID(ctx.$app, id);
37
+ const file = await SealiousFile.fromID(ctx.$app, id);
38
38
  return tempstream/* HTML */ `<div class="${class_name}">
39
39
  ${this.imageRouter.image({
40
40
  resolutions: [100],
@@ -5,13 +5,13 @@ import {
5
5
  predicates,
6
6
  } from "@sealcode/ts-predicates";
7
7
  import { Context } from "koa";
8
- import Sealious from "sealious";
8
+ import { File as SealiousFile } from "sealious";
9
9
  import { FormDataValue } from "../form-types.js";
10
10
  import { FieldParseResult, FormField } from "./field.js";
11
11
 
12
12
  export type FileContainer = {
13
- old: Sealious.File | null;
14
- new: Sealious.File | null;
13
+ old: SealiousFile | null;
14
+ new: SealiousFile | null;
15
15
  };
16
16
 
17
17
  export class File<Required extends boolean> extends FormField<
@@ -24,7 +24,7 @@ export class File<Required extends boolean> extends FormField<
24
24
  ): Promise<FieldParseResult<FileContainer>> {
25
25
  let result;
26
26
  if (
27
- raw_value instanceof Sealious.File ||
27
+ raw_value instanceof SealiousFile ||
28
28
  (is(raw_value, predicates.object) &&
29
29
  hasShape(
30
30
  {
@@ -35,7 +35,7 @@ export class File<Required extends boolean> extends FormField<
35
35
  ) &&
36
36
  raw_value.id)
37
37
  ) {
38
- const file = await Sealious.File.fromID(
38
+ const file = await SealiousFile.fromID(
39
39
  ctx.$app,
40
40
  raw_value.id as string
41
41
  );
@@ -58,14 +58,14 @@ export class File<Required extends boolean> extends FormField<
58
58
  raw_value.old
59
59
  ) &&
60
60
  raw_value.old.id
61
- ? await Sealious.File.fromID(ctx.$app, raw_value.old.id)
61
+ ? await SealiousFile.fromID(ctx.$app, raw_value.old.id)
62
62
  : null;
63
63
  const new_file =
64
64
  hasFieldOfType(
65
65
  raw_value,
66
66
  "new",
67
67
  predicates.array(predicates.object)
68
- ) && raw_value.new[0] instanceof Sealious.File
68
+ ) && raw_value.new[0] instanceof SealiousFile
69
69
  ? raw_value.new[0]
70
70
  : null;
71
71
  result = {
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- import Sealious, { App } from "sealious";
1
+ import { App, Context as SealiousContext } from "sealious";
2
2
  import { FormDataValue } from "./forms/form-types.js";
3
3
 
4
4
  declare module "koa" {
5
5
  interface BaseContext {
6
- $context: Sealious.Context;
6
+ $context: SealiousContext;
7
7
  $app: App;
8
8
  $body: Record<string, FormDataValue>;
9
9
  }
package/src/mount.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import Router from "@koa/router";
2
2
  import Koa, { BaseContext, Context } from "koa";
3
- import Sealious from "sealious";
3
+ import { Middlewares as SealiousMiddlewares } from "sealious";
4
4
  import { Mountable } from "./page/mountable.js";
5
5
 
6
6
  async function handleHtmlPromise(ctx: Context, next: Koa.Next) {
@@ -48,12 +48,12 @@ export function mount(
48
48
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
49
49
  await next();
50
50
  },
51
- Sealious.Middlewares.parseBody(),
51
+ SealiousMiddlewares.parseBody(),
52
52
  handleHtmlPromise,
53
53
  ]
54
54
  : [
55
- Sealious.Middlewares.extractContext(),
56
- Sealious.Middlewares.parseBody(),
55
+ SealiousMiddlewares.extractContext(),
56
+ SealiousMiddlewares.parseBody(),
57
57
  handleHtmlPromise,
58
58
  ];
59
59
 
@@ -1,5 +1,5 @@
1
1
  import Router from "@koa/router";
2
- import { predicates, hasShape, is } from "@sealcode/ts-predicates";
2
+ import { predicates, hasShape, is, hasField } from "@sealcode/ts-predicates";
3
3
  import deepmerge, { ArrayMergeOptions } from "deepmerge";
4
4
  import { BaseContext } from "koa";
5
5
  import { Templatable, tempstream } from "tempstream";
@@ -14,6 +14,21 @@ export type StatefulPageActionDescription<ActionName> =
14
14
  }
15
15
  | ActionName;
16
16
 
17
+ export type StateAndMetadata<State, Actions> =
18
+ | {
19
+ state: State;
20
+ inputs: Record<string, string>;
21
+ action: keyof Actions;
22
+ action_args: string;
23
+ }
24
+ | {
25
+ state: State;
26
+ inputs: Record<string, string>;
27
+ action: null;
28
+ action_args: null;
29
+ $: Record<string, unknown>;
30
+ };
31
+
17
32
  export abstract class StatefulPage<
18
33
  State extends Record<string, unknown>,
19
34
  Actions extends Record<
@@ -22,11 +37,11 @@ export abstract class StatefulPage<
22
37
  state: State,
23
38
  inputs: Record<string, string>,
24
39
  ...args: unknown[]
25
- ) => State
40
+ ) => State | Promise<State>
26
41
  >
27
42
  > extends Mountable {
28
43
  abstract actions: Actions;
29
- abstract getInitialState(): State;
44
+ abstract getInitialState(): State | Promise<State>;
30
45
  abstract render(
31
46
  ctx: BaseContext,
32
47
  state: State,
@@ -87,11 +102,13 @@ export abstract class StatefulPage<
87
102
  ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
88
103
  ) {
89
104
  const { action } = this.extractActionAndLabel(action_description);
90
- return `./?action=${action}&args=${to_base64(JSON.stringify(args))}`;
105
+ return `./?action=${action}&action_args=${to_base64(
106
+ JSON.stringify(args)
107
+ )}`;
91
108
  }
92
109
 
93
110
  makeActionButton<ActionName extends keyof Actions>(
94
- state: State,
111
+ _state: State,
95
112
  action_description: StatefulPageActionDescription<ActionName>,
96
113
  ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
97
114
  ) {
@@ -99,7 +116,7 @@ export abstract class StatefulPage<
99
116
  return /* HTML */ `
100
117
  <input
101
118
  type="submit"
102
- value=${label}
119
+ value="${label}"
103
120
  formaction="${this.makeActionURL(action_description, ...args)}"
104
121
  />
105
122
  `;
@@ -124,25 +141,21 @@ export abstract class StatefulPage<
124
141
  return "this.closest('form').requestSubmit()";
125
142
  }
126
143
 
127
- extractState(ctx: BaseContext):
128
- | {
129
- state: State;
130
- inputs: Record<string, string>;
131
- action: keyof Actions;
132
- action_args: string;
133
- }
134
- | {
135
- state: State;
136
- inputs: Record<string, string>;
137
- action: null;
138
- action_args: null;
139
- } {
144
+ async preprocessRequestBody(
145
+ values: Record<string, unknown>
146
+ ): Promise<StateAndMetadata<State, Actions>> {
147
+ return values as StateAndMetadata<State, Actions>;
148
+ }
149
+
150
+ async extractState(
151
+ ctx: BaseContext
152
+ ): Promise<StateAndMetadata<State, Actions>> {
140
153
  if (
141
154
  !hasShape(
142
155
  {
143
156
  action: predicates.maybe(predicates.string),
144
157
  state: predicates.string,
145
- args: predicates.maybe(predicates.string),
158
+ action_args: predicates.maybe(predicates.string),
146
159
  $: predicates.maybe(predicates.object),
147
160
  },
148
161
  ctx.$body
@@ -151,55 +164,67 @@ export abstract class StatefulPage<
151
164
  console.error("Wrong data: ", ctx.$body);
152
165
  throw new Error("wrong formdata shape");
153
166
  }
167
+ // giving extending classes a change to modify body before furhter processing
168
+ const prepared_body = await this.preprocessRequestBody(ctx.$body);
154
169
  const inputs = Object.fromEntries(
155
- Object.entries(ctx.$body).filter(
170
+ Object.entries(prepared_body).filter(
156
171
  ([key]) => !["action", "state", "args", "$"].includes(key)
157
172
  )
158
173
  ) as Record<string, string>;
159
174
  // the "$" key is parsed as dot notation and overrides the state
160
175
 
161
- const original_state = JSON.parse(from_base64(ctx.$body.state));
162
-
163
- const modified_state = deepmerge(original_state, ctx.$body.$ || {}, {
164
- arrayMerge: (
165
- _target: any[],
166
- source: any[],
167
- _options: ArrayMergeOptions
168
- ) => {
169
- // https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
170
- return source;
171
- },
172
- }) as State;
176
+ const original_state_string = prepared_body.state;
177
+ const original_state = JSON.parse(
178
+ typeof original_state_string == "string"
179
+ ? from_base64(original_state_string)
180
+ : "{}"
181
+ );
182
+
183
+ const modified_state = deepmerge(
184
+ original_state,
185
+ hasField("$", prepared_body) ? prepared_body.$ : {},
186
+ {
187
+ arrayMerge: (
188
+ _target: any[],
189
+ source: any[],
190
+ _options: ArrayMergeOptions
191
+ ) => {
192
+ // https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
193
+ return source;
194
+ },
195
+ }
196
+ ) as State;
173
197
 
174
- if (ctx.$body.action && ctx.$body.args) {
198
+ if (prepared_body.action && prepared_body.action_args) {
175
199
  return {
176
200
  state: modified_state,
177
- action: ctx.$body.action as string,
201
+ action: prepared_body.action as string,
178
202
  inputs,
179
- action_args: ctx.$body.args,
203
+ action_args: prepared_body.action_args,
180
204
  };
181
205
  } else {
182
206
  return {
183
207
  state: modified_state,
184
208
  action: null,
185
- action_args: null,
186
209
  inputs,
210
+ action_args: null,
211
+ $: hasField("$", prepared_body) ? prepared_body.$ : {},
187
212
  };
188
213
  }
189
214
  }
190
215
 
191
216
  mount(router: Router, path: string) {
192
- router.get(path, (ctx) => {
193
- ctx.body = this.render(ctx, this.getInitialState(), {});
217
+ router.get(path, async (ctx) => {
218
+ ctx.body = this.render(ctx, await this.getInitialState(), {});
194
219
  });
195
220
 
196
- router.post(path, (ctx) => {
221
+ router.post(path, async (ctx) => {
197
222
  const { action, state, inputs, action_args } =
198
- this.extractState(ctx);
223
+ await this.extractState(ctx);
199
224
  if (action) {
200
225
  ctx.body = this.render(
201
226
  ctx,
202
- this.actions[action](
227
+ await this.actions[action](
203
228
  state,
204
229
  inputs,
205
230
  ...(JSON.parse(