@sealcode/sealgen 0.12.12 → 0.13.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.
- package/@types/src/get-fonts.d.ts +2 -0
- package/@types/src/page/stateful-page.d.ts +17 -14
- package/@types/src/target-locreq.d.ts +4 -0
- package/dist/build.js +10 -1
- package/dist/cli.js +3 -1
- package/dist/get-fonts.js +27 -0
- package/dist/page/stateful-page.js +35 -21
- package/dist/target-locreq.js +6 -0
- package/package.json +2 -1
- package/src/build.ts +12 -3
- package/src/cli.ts +2 -0
- package/src/get-fonts.ts +25 -0
- package/src/page/stateful-page.ts +67 -42
- package/src/target-locreq.ts +2 -0
|
@@ -8,9 +8,21 @@ export type StatefulPageActionDescription<ActionName> = {
|
|
|
8
8
|
action: ActionName;
|
|
9
9
|
label?: string;
|
|
10
10
|
} | ActionName;
|
|
11
|
-
export
|
|
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>(
|
|
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
|
-
|
|
34
|
-
|
|
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
|
}
|
package/dist/build.js
CHANGED
|
@@ -14,6 +14,7 @@ import md5 from "md5";
|
|
|
14
14
|
import _locreq from "locreq";
|
|
15
15
|
import path, { basename, extname } from "path";
|
|
16
16
|
import { copyFile } from "fs/promises";
|
|
17
|
+
import { FONTS_CONFIG_PATH, getFonts } from "./get-fonts.js";
|
|
17
18
|
const target_locreq = _locreq(process.cwd());
|
|
18
19
|
async function get_notifier_port() {
|
|
19
20
|
return getPort({ port: 4e3 });
|
|
@@ -113,7 +114,6 @@ async function build_ts() {
|
|
|
113
114
|
)
|
|
114
115
|
},
|
|
115
116
|
async (args) => {
|
|
116
|
-
const mode = "url";
|
|
117
117
|
let contents = `export const url = "${await makeResourceURL(
|
|
118
118
|
args.path
|
|
119
119
|
)}";
|
|
@@ -167,6 +167,7 @@ async function complex_build(watch) {
|
|
|
167
167
|
// plugins: [sassPlugin()],
|
|
168
168
|
loader
|
|
169
169
|
});
|
|
170
|
+
let ongoing_fonts = false;
|
|
170
171
|
let ongoing_ts = false;
|
|
171
172
|
let ongoing_css = false;
|
|
172
173
|
let ongoing_ts_promise = null;
|
|
@@ -180,6 +181,14 @@ async function complex_build(watch) {
|
|
|
180
181
|
return;
|
|
181
182
|
if (path2.includes(".#"))
|
|
182
183
|
return;
|
|
184
|
+
if (path2 == FONTS_CONFIG_PATH) {
|
|
185
|
+
if (!ongoing_fonts) {
|
|
186
|
+
ongoing_fonts = true;
|
|
187
|
+
await getFonts();
|
|
188
|
+
ongoing_fonts = false;
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
183
192
|
if (path2.endsWith(".css") && !path2.endsWith("/includes.css")) {
|
|
184
193
|
if (ongoing_css) {
|
|
185
194
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { generateComponents } from "./generate-components.js";
|
|
|
9
9
|
import { generateRoutes } from "./generate-routes.js";
|
|
10
10
|
import { makeEnv } from "./make-env.js";
|
|
11
11
|
import yargs from "yargs/yargs";
|
|
12
|
+
import { getFonts } from "./get-fonts.js";
|
|
12
13
|
const actions = {
|
|
13
14
|
"add-collection": addCollection,
|
|
14
15
|
"add-route": addRoute,
|
|
@@ -24,7 +25,8 @@ const actions = {
|
|
|
24
25
|
);
|
|
25
26
|
},
|
|
26
27
|
"make-env": makeEnv,
|
|
27
|
-
"add-component": addJDDComponent
|
|
28
|
+
"add-component": addJDDComponent,
|
|
29
|
+
"get-fonts": getFonts
|
|
28
30
|
};
|
|
29
31
|
void async function() {
|
|
30
32
|
const action = process.argv.at(2);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { constructURL, download } from "google-fonts-helper";
|
|
3
|
+
import { target_locreq } from "./target-locreq.js";
|
|
4
|
+
const FONTS_CONFIG_PATH = "src/fonts.json";
|
|
5
|
+
async function getFonts(_ = {}) {
|
|
6
|
+
const fonts_config_path = target_locreq.resolve(FONTS_CONFIG_PATH);
|
|
7
|
+
const fonts_config = JSON.parse(await readFile(fonts_config_path, "utf8"));
|
|
8
|
+
const fonts_url = constructURL(fonts_config.googleFonts);
|
|
9
|
+
if (!fonts_url) {
|
|
10
|
+
throw new Error("Invalid font config");
|
|
11
|
+
}
|
|
12
|
+
const downloader = download(fonts_url, {
|
|
13
|
+
base64: false,
|
|
14
|
+
overwriting: false,
|
|
15
|
+
outputDir: target_locreq.resolve("public/dist/fonts"),
|
|
16
|
+
stylePath: "fonts.css",
|
|
17
|
+
fontsDir: "./",
|
|
18
|
+
fontsPath: "/dist/fonts"
|
|
19
|
+
});
|
|
20
|
+
await downloader.execute();
|
|
21
|
+
console.log("Downloaded new fonts pack");
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
FONTS_CONFIG_PATH,
|
|
25
|
+
getFonts
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=get-fonts.js.map
|
|
@@ -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}&
|
|
46
|
+
return `./?action=${action}&action_args=${to_base64(
|
|
47
|
+
JSON.stringify(args)
|
|
48
|
+
)}`;
|
|
47
49
|
}
|
|
48
|
-
makeActionButton(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
90
|
+
Object.entries(prepared_body).filter(
|
|
85
91
|
([key]) => !["action", "state", "args", "$"].includes(key)
|
|
86
92
|
)
|
|
87
93
|
);
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
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 (
|
|
106
|
+
);
|
|
107
|
+
if (prepared_body.action && prepared_body.action_args) {
|
|
95
108
|
return {
|
|
96
109
|
state: modified_state,
|
|
97
|
-
action:
|
|
110
|
+
action: prepared_body.action,
|
|
98
111
|
inputs,
|
|
99
|
-
action_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
|
-
|
|
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.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@sealcode/ts-predicates": "^0.4.3",
|
|
56
56
|
"deepmerge": "^4.3.1",
|
|
57
57
|
"esbuild": "^0.20.0",
|
|
58
|
+
"google-fonts-helper": "^3.4.1",
|
|
58
59
|
"js-convert-case": "^4.2.0",
|
|
59
60
|
"locreq": "^3.0.0",
|
|
60
61
|
"md5": "^2.3.0",
|
package/src/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { build, BuildContext,
|
|
1
|
+
import { build, BuildContext, context } from "esbuild";
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import * as chokidar from "chokidar";
|
|
4
4
|
import { default as glob } from "tiny-glob";
|
|
@@ -14,7 +14,8 @@ import md5 from "md5";
|
|
|
14
14
|
|
|
15
15
|
import _locreq from "locreq";
|
|
16
16
|
import path, { basename, extname } from "path";
|
|
17
|
-
import { copyFile
|
|
17
|
+
import { copyFile } from "fs/promises";
|
|
18
|
+
import { FONTS_CONFIG_PATH, getFonts } from "./get-fonts.js";
|
|
18
19
|
const target_locreq = _locreq(process.cwd());
|
|
19
20
|
|
|
20
21
|
async function get_notifier_port() {
|
|
@@ -127,7 +128,6 @@ async function build_ts() {
|
|
|
127
128
|
),
|
|
128
129
|
},
|
|
129
130
|
async (args: any) => {
|
|
130
|
-
const mode = "url";
|
|
131
131
|
let contents = `export const url = "${await makeResourceURL(
|
|
132
132
|
args.path
|
|
133
133
|
)}";
|
|
@@ -183,6 +183,7 @@ async function complex_build(watch: boolean): Promise<void> {
|
|
|
183
183
|
// plugins: [sassPlugin()],
|
|
184
184
|
loader,
|
|
185
185
|
});
|
|
186
|
+
let ongoing_fonts = false;
|
|
186
187
|
let ongoing_ts = false;
|
|
187
188
|
let ongoing_css = false;
|
|
188
189
|
let ongoing_ts_promise: Promise<any> | null = null;
|
|
@@ -194,6 +195,14 @@ async function complex_build(watch: boolean): Promise<void> {
|
|
|
194
195
|
watcher.on("all", async (_, path) => {
|
|
195
196
|
if (!css_build) return;
|
|
196
197
|
if (path.includes(".#")) return;
|
|
198
|
+
if (path == FONTS_CONFIG_PATH) {
|
|
199
|
+
if (!ongoing_fonts) {
|
|
200
|
+
ongoing_fonts = true;
|
|
201
|
+
await getFonts();
|
|
202
|
+
ongoing_fonts = false;
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
197
206
|
if (path.endsWith(".css") && !path.endsWith("/includes.css")) {
|
|
198
207
|
if (ongoing_css) {
|
|
199
208
|
return;
|
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { generateComponents } from "./generate-components.js";
|
|
|
9
9
|
import { generateRoutes } from "./generate-routes.js";
|
|
10
10
|
import { makeEnv } from "./make-env.js";
|
|
11
11
|
import yargs from "yargs/yargs";
|
|
12
|
+
import { getFonts } from "./get-fonts.js";
|
|
12
13
|
|
|
13
14
|
const actions: Record<
|
|
14
15
|
string,
|
|
@@ -31,6 +32,7 @@ const actions: Record<
|
|
|
31
32
|
},
|
|
32
33
|
"make-env": makeEnv,
|
|
33
34
|
"add-component": addJDDComponent,
|
|
35
|
+
"get-fonts": getFonts,
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
void (async function () {
|
package/src/get-fonts.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { constructURL, download } from "google-fonts-helper";
|
|
3
|
+
import { target_locreq } from "./target-locreq.js";
|
|
4
|
+
|
|
5
|
+
export const FONTS_CONFIG_PATH = "src/fonts.json";
|
|
6
|
+
|
|
7
|
+
export async function getFonts(_: Record<string, string | boolean> = {}) {
|
|
8
|
+
const fonts_config_path = target_locreq.resolve(FONTS_CONFIG_PATH);
|
|
9
|
+
|
|
10
|
+
const fonts_config = JSON.parse(await readFile(fonts_config_path, "utf8"));
|
|
11
|
+
const fonts_url = constructURL(fonts_config.googleFonts);
|
|
12
|
+
if (!fonts_url) {
|
|
13
|
+
throw new Error("Invalid font config");
|
|
14
|
+
}
|
|
15
|
+
const downloader = download(fonts_url, {
|
|
16
|
+
base64: false,
|
|
17
|
+
overwriting: false,
|
|
18
|
+
outputDir: target_locreq.resolve("public/dist/fonts"),
|
|
19
|
+
stylePath: "fonts.css",
|
|
20
|
+
fontsDir: "./",
|
|
21
|
+
fontsPath: "/dist/fonts",
|
|
22
|
+
});
|
|
23
|
+
await downloader.execute();
|
|
24
|
+
console.log("Downloaded new fonts pack");
|
|
25
|
+
}
|
|
@@ -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}&
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 (
|
|
198
|
+
if (prepared_body.action && prepared_body.action_args) {
|
|
175
199
|
return {
|
|
176
200
|
state: modified_state,
|
|
177
|
-
action:
|
|
201
|
+
action: prepared_body.action as string,
|
|
178
202
|
inputs,
|
|
179
|
-
action_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(
|