@sealcode/sealgen 0.7.15 → 0.8.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.
Files changed (104) hide show
  1. package/@types/forms/controls/big-radio-group.d.ts +15 -0
  2. package/@types/forms/controls/checkbox.d.ts +8 -0
  3. package/@types/forms/controls/checkboxed-list-input.d.ts +2 -2
  4. package/@types/forms/controls/controls.d.ts +5 -0
  5. package/@types/forms/controls/derived.d.ts +11 -0
  6. package/@types/forms/controls/dropdown.d.ts +2 -2
  7. package/@types/forms/controls/editable-collection-subset.d.ts +2 -2
  8. package/@types/forms/controls/html.d.ts +6 -3
  9. package/@types/forms/controls/radio.d.ts +16 -0
  10. package/@types/forms/controls/tickable.d.ts +26 -0
  11. package/@types/forms/fields/boolean.d.ts +17 -0
  12. package/@types/forms/fields/checkboxed-list.d.ts +7 -3
  13. package/@types/forms/fields/collection-field.d.ts +6 -2
  14. package/@types/forms/fields/email.d.ts +3 -2
  15. package/@types/forms/fields/enum.d.ts +8 -4
  16. package/@types/forms/fields/field.d.ts +12 -4
  17. package/@types/forms/fields/number.d.ts +3 -2
  18. package/@types/forms/fields/pick-from-list.d.ts +6 -5
  19. package/@types/forms/fields/regexp.d.ts +3 -2
  20. package/@types/forms/fields/simple-form-field.d.ts +3 -1
  21. package/@types/forms/form.d.ts +6 -5
  22. package/@types/forms/multiform.d.ts +2 -2
  23. package/@types/page/list.d.ts +2 -2
  24. package/@types/page/mountable-with-fields.d.ts +4 -2
  25. package/lib/forms/controls/big-radio-group.js +37 -0
  26. package/lib/forms/controls/big-radio-group.js.map +1 -0
  27. package/lib/forms/controls/checkbox.js +18 -0
  28. package/lib/forms/controls/checkbox.js.map +1 -0
  29. package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
  30. package/lib/forms/controls/controls.js +15 -0
  31. package/lib/forms/controls/controls.js.map +1 -1
  32. package/lib/forms/controls/derived.js +70 -0
  33. package/lib/forms/controls/derived.js.map +1 -0
  34. package/lib/forms/controls/dropdown.js.map +1 -1
  35. package/lib/forms/controls/editable-collection-subset.js.map +1 -1
  36. package/lib/forms/controls/html.js +11 -3
  37. package/lib/forms/controls/html.js.map +1 -1
  38. package/lib/forms/controls/radio.js +29 -0
  39. package/lib/forms/controls/radio.js.map +1 -0
  40. package/lib/forms/controls/simple-input.js +3 -1
  41. package/lib/forms/controls/simple-input.js.map +1 -1
  42. package/lib/forms/controls/textarea.js +3 -1
  43. package/lib/forms/controls/textarea.js.map +1 -1
  44. package/lib/forms/controls/tickable.js +72 -0
  45. package/lib/forms/controls/tickable.js.map +1 -0
  46. package/lib/forms/fields/boolean.js +45 -0
  47. package/lib/forms/fields/boolean.js.map +1 -0
  48. package/lib/forms/fields/checkboxed-list.js +2 -1
  49. package/lib/forms/fields/checkboxed-list.js.map +1 -1
  50. package/lib/forms/fields/collection-field.js +2 -0
  51. package/lib/forms/fields/collection-field.js.map +1 -1
  52. package/lib/forms/fields/email.js +3 -1
  53. package/lib/forms/fields/email.js.map +1 -1
  54. package/lib/forms/fields/enum.js +2 -1
  55. package/lib/forms/fields/enum.js.map +1 -1
  56. package/lib/forms/fields/field.js +9 -2
  57. package/lib/forms/fields/field.js.map +1 -1
  58. package/lib/forms/fields/number.js +2 -1
  59. package/lib/forms/fields/number.js.map +1 -1
  60. package/lib/forms/fields/pick-from-list.js +2 -1
  61. package/lib/forms/fields/pick-from-list.js.map +1 -1
  62. package/lib/forms/fields/regexp.js +2 -1
  63. package/lib/forms/fields/regexp.js.map +1 -1
  64. package/lib/forms/fields/simple-form-field.js +5 -0
  65. package/lib/forms/fields/simple-form-field.js.map +1 -1
  66. package/lib/forms/form.js +9 -5
  67. package/lib/forms/form.js.map +1 -1
  68. package/lib/forms/multiform.js.map +1 -1
  69. package/lib/mount.js +4 -2
  70. package/lib/mount.js.map +1 -1
  71. package/lib/page/mountable-with-fields.js +10 -0
  72. package/lib/page/mountable-with-fields.js.map +1 -1
  73. package/lib/templates/form.js +3 -1
  74. package/lib/templates/form.js.map +1 -1
  75. package/lib/tests/manual/multiform.js.map +1 -1
  76. package/package.json +2 -2
  77. package/src/forms/controls/big-radio-group.ts +63 -0
  78. package/src/forms/controls/checkbox.ts +19 -0
  79. package/src/forms/controls/checkboxed-list-input.ts +1 -1
  80. package/src/forms/controls/controls.ts +5 -0
  81. package/src/forms/controls/derived.ts +81 -0
  82. package/src/forms/controls/dropdown.ts +1 -1
  83. package/src/forms/controls/editable-collection-subset.ts +1 -1
  84. package/src/forms/controls/html.ts +21 -3
  85. package/src/forms/controls/radio.ts +43 -0
  86. package/src/forms/controls/simple-input.ts +4 -1
  87. package/src/forms/controls/textarea.ts +3 -1
  88. package/src/forms/controls/tickable.ts +112 -0
  89. package/src/forms/fields/boolean.ts +55 -0
  90. package/src/forms/fields/checkboxed-list.ts +7 -2
  91. package/src/forms/fields/collection-field.ts +8 -4
  92. package/src/forms/fields/email.ts +6 -2
  93. package/src/forms/fields/enum.ts +9 -4
  94. package/src/forms/fields/field.ts +32 -3
  95. package/src/forms/fields/number.ts +6 -3
  96. package/src/forms/fields/pick-from-list.ts +7 -3
  97. package/src/forms/fields/regexp.ts +4 -2
  98. package/src/forms/fields/simple-form-field.ts +11 -1
  99. package/src/forms/form.ts +27 -13
  100. package/src/forms/multiform.ts +4 -2
  101. package/src/mount.ts +4 -2
  102. package/src/page/mountable-with-fields.ts +18 -2
  103. package/src/templates/form.ts +3 -1
  104. package/src/tests/manual/multiform.ts +2 -2
@@ -3,16 +3,21 @@ import { Context } from "koa";
3
3
  import { FormDataValue } from "../form";
4
4
  import { FieldParseResult, FormField } from "./field";
5
5
 
6
- export class EnumField extends FormField<string> {
6
+ export class EnumField<
7
+ Options extends readonly string[],
8
+ Required extends boolean
9
+ > extends FormField<Required, string> {
7
10
  constructor(
8
- required = false,
9
- public options: string[],
10
- public error_message: (options: string[]) => string = (options) =>
11
+ required: Required,
12
+ public options: Options,
13
+ public error_message: (options: Options) => string = (options) =>
11
14
  options.join(", ")
12
15
  ) {
13
16
  super(required);
14
17
  }
15
18
 
19
+ predicate = predicates.enum<Options>(this.options);
20
+
16
21
  public async isValueValid(
17
22
  _: Context,
18
23
  value: unknown
@@ -1,17 +1,41 @@
1
+ import { Predicate, predicates } from "@sealcode/ts-predicates";
1
2
  import { Context } from "koa";
2
3
  import { t } from "../../utils/translate";
3
4
  import { FormDataValue } from "../form";
5
+ import { SimpleFormField } from "./simple-form-field";
4
6
 
5
7
  export type FormFieldValidationResponse = { valid: boolean; message: string };
6
8
 
7
9
  export type FormFieldValidationFn<ValueType> = (
8
10
  ctx: Context,
9
11
  value: ValueType,
10
- field: FormField<ValueType>
12
+ field: FormField<boolean, ValueType>
11
13
  ) => Promise<FormFieldValidationResponse>;
12
14
 
13
15
  export type FieldParsedValue<T> = T extends FormField<infer R> ? R : never;
14
16
 
17
+ export type GetPredicate<F extends FormField> = F extends FormField<true>
18
+ ? F["predicate"]
19
+ : F["predicate"] | typeof predicates.undefined;
20
+
21
+ // for quickly testing GetPredicate conditonally applying the undefined predicate:
22
+ // const f = new SimpleFormField(true);
23
+ // type p = GetPredicate<typeof f>;
24
+ // const f2 = new SimpleFormField(false);
25
+ // type p2 = GetPredicate<typeof f2>;
26
+
27
+ export type FieldsToShape<T extends Record<string, FormField>> = {
28
+ [property in keyof T]: GetPredicate<T[property]>;
29
+ };
30
+
31
+ export function fieldsToShape<Fields extends Record<string, FormField>>(
32
+ obj: Fields
33
+ ): FieldsToShape<Fields> {
34
+ return Object.fromEntries(
35
+ Object.entries(obj).map(([key, value]) => [key, value.predicate])
36
+ ) as FieldsToShape<Fields>;
37
+ }
38
+
15
39
  export type FieldParseResult<T> =
16
40
  | {
17
41
  parsable: false;
@@ -20,9 +44,13 @@ export type FieldParseResult<T> =
20
44
  }
21
45
  | { parsable: true; error: null; parsed_value: T };
22
46
 
23
- export abstract class FormField<ParsedValue extends unknown = unknown> {
47
+ export abstract class FormField<
48
+ Required extends boolean = boolean,
49
+ ParsedValue extends unknown = unknown
50
+ > {
24
51
  name: string;
25
- constructor(public required: boolean = false) {}
52
+ constructor(public readonly required: Required) {}
53
+ predicate: Predicate = predicates.unknown;
26
54
 
27
55
  init(fieldname: string): void {
28
56
  this.name = fieldname;
@@ -117,3 +145,4 @@ export * from "./pick-from-list";
117
145
  export * from "./regexp";
118
146
  export * from "./email";
119
147
  export * from "./enum";
148
+ export * from "./boolean";
@@ -4,12 +4,15 @@ import { FormDataValue } from "../form";
4
4
  import { FieldParseResult, FormField } from "./field";
5
5
 
6
6
  export class NumberField<
7
- DefaultValue extends number | null = null
8
- > extends FormField<number | DefaultValue> {
9
- constructor(required = false, public default_value: DefaultValue) {
7
+ DefaultValue extends number | null = null,
8
+ Required extends boolean = boolean
9
+ > extends FormField<Required, number | DefaultValue> {
10
+ constructor(required: Required, public default_value: DefaultValue) {
10
11
  super(required);
11
12
  }
12
13
 
14
+ predicate = predicates.number;
15
+
13
16
  public async isValueValid(_: Context, value: unknown) {
14
17
  if (
15
18
  (is(value, predicates.string) &&
@@ -3,22 +3,26 @@ import { Context } from "koa";
3
3
  import { FormFieldValidationResponse } from "./field";
4
4
  import { SimpleFormField } from "./simple-form-field";
5
5
 
6
- export class PickFromListField extends SimpleFormField {
6
+ export class PickFromListField<
7
+ Required extends boolean
8
+ > extends SimpleFormField<Required> {
7
9
  constructor(
8
- public required: boolean = false,
10
+ public required: Required,
9
11
  public generateOptions: (
10
12
  ctx: Context
11
13
  ) => Promise<Record<string, string> | { [i: string]: string }>,
12
14
  public customValidation: (
13
15
  ctx: Context,
14
16
  value: unknown,
15
- instance: PickFromListField
17
+ instance: PickFromListField<Required>
16
18
  ) => Promise<FormFieldValidationResponse> = (ctx, value, instance) =>
17
19
  instance.valueInList(ctx, value)
18
20
  ) {
19
21
  super(required);
20
22
  }
21
23
 
24
+ predicate = predicates.string;
25
+
22
26
  public isValueValid(
23
27
  ctx: Context,
24
28
  value: string
@@ -3,9 +3,9 @@ import { Context } from "koa";
3
3
  import { FormDataValue } from "../form";
4
4
  import { FieldParseResult, FormField } from "./field";
5
5
 
6
- export class RegExpField extends FormField<string> {
6
+ export class RegExpField<Required extends boolean> extends FormField<Required> {
7
7
  constructor(
8
- required = false,
8
+ required: Required,
9
9
  public regexp: RegExp,
10
10
  public error_message: string,
11
11
  public default_value: string = ""
@@ -13,6 +13,8 @@ export class RegExpField extends FormField<string> {
13
13
  super(required);
14
14
  }
15
15
 
16
+ predicate = predicates.string;
17
+
16
18
  public async isValueValid(
17
19
  _: Context,
18
20
  value: unknown
@@ -1,8 +1,12 @@
1
1
  import { FieldParseResult, FormField } from "./field";
2
2
  import { FormDataValue } from "../form";
3
3
  import { Context } from "koa";
4
+ import { predicates } from "@sealcode/ts-predicates";
4
5
 
5
- export class SimpleFormField extends FormField<string | null> {
6
+ export class SimpleFormField<Required extends boolean> extends FormField<
7
+ Required,
8
+ string | null
9
+ > {
6
10
  async parse(
7
11
  _: Context,
8
12
  raw_value: FormDataValue
@@ -14,7 +18,13 @@ export class SimpleFormField extends FormField<string | null> {
14
18
  };
15
19
  }
16
20
 
21
+ predicate = predicates.string;
22
+
17
23
  public getEmptyValue(): string {
18
24
  return "";
19
25
  }
26
+
27
+ constructor(required: Required) {
28
+ super(required);
29
+ }
20
30
  }
package/src/forms/form.ts CHANGED
@@ -7,14 +7,14 @@ import {
7
7
  is,
8
8
  predicates,
9
9
  } from "@sealcode/ts-predicates";
10
- import { FormField } from "./fields/field";
10
+ import { FieldsToShape, FormField } from "./fields/field";
11
11
  import {
12
12
  Fields,
13
13
  MountableWithFields,
14
14
  PageErrorMessage,
15
15
  } from "../page/mountable-with-fields";
16
16
 
17
- export type FormDataPrimitive = string | string[] | undefined;
17
+ export type FormDataPrimitive = string | string[] | number | undefined;
18
18
  export type FormDataValue =
19
19
  | FormDataPrimitive
20
20
  | Record<string, FormDataPrimitive>;
@@ -23,7 +23,9 @@ export type FieldValueType<F extends FormField> = F extends FormField<infer R>
23
23
  ? R
24
24
  : never;
25
25
 
26
- export type FormFieldsToValues<F extends Record<string, FormField<keyof F>>> = {
26
+ export type FormFieldsToValues<
27
+ F extends Record<string, FormField<boolean, keyof F>>
28
+ > = {
27
29
  [Property in keyof F]: FieldValueType<F[Property]>;
28
30
  };
29
31
 
@@ -38,10 +40,14 @@ export type FormReaction =
38
40
  | { action: "stay"; content: FlatTemplatable; messages?: FormMessage[] }
39
41
  | { action: "redirect"; url: string; messages?: FormMessage[] };
40
42
 
41
- export abstract class Form<F extends Fields> extends MountableWithFields<F> {
43
+ export abstract class Form<
44
+ F extends Fields,
45
+ SubmitResult extends any
46
+ > extends MountableWithFields<F> {
42
47
  defaultSuccessMessage = "Done";
43
48
  submitButtonText = "Wyślij";
44
49
  action = "./";
50
+ useTurbo = true;
45
51
 
46
52
  async canAccess(
47
53
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -71,7 +77,10 @@ export abstract class Form<F extends Fields> extends MountableWithFields<F> {
71
77
  ${!this.controls.some((control) => control.role == "messages")
72
78
  ? this.renderMessages(ctx, data)
73
79
  : ""}
74
- ${this.renderControls(ctx, data)} ${this.makeSubmitButton()}
80
+ ${this.renderControls(ctx, data)}
81
+ ${this.controls.some((control) => control.role == "submit")
82
+ ? ""
83
+ : this.makeSubmitButton()}
75
84
  ${this.makeCloseFormTag()}`;
76
85
  }
77
86
 
@@ -85,7 +94,7 @@ export abstract class Form<F extends Fields> extends MountableWithFields<F> {
85
94
  this.action
86
95
  }" class="${this.makeFormClasses(ctx).then((classes) =>
87
96
  classes.join(" ")
88
- )}">`;
97
+ )}" ${this.useTurbo ? "" : `data-turbo="false"`}>`;
89
98
  }
90
99
 
91
100
  public makeCloseFormTag(): FlatTemplatable {
@@ -135,11 +144,12 @@ export abstract class Form<F extends Fields> extends MountableWithFields<F> {
135
144
  public abstract onSubmit(
136
145
  ctx: Context,
137
146
  data: FormData
138
- ): void | Promise<void>;
147
+ ): SubmitResult | Promise<SubmitResult>;
139
148
 
140
149
  public async onSuccess(
141
150
  ctx: Context,
142
- data: FormData
151
+ data: FormData,
152
+ _submitResult: SubmitResult
143
153
  ): Promise<FormReaction> {
144
154
  const messages = [
145
155
  <const>{ type: "success", text: this.defaultSuccessMessage },
@@ -164,14 +174,18 @@ export abstract class Form<F extends Fields> extends MountableWithFields<F> {
164
174
  }
165
175
  try {
166
176
  ctx.status = 303;
167
- await this.onSubmit(ctx, {
168
- raw_values: this.extractRawValues(ctx),
169
- messages: [],
170
- });
171
- return this.onSuccess(ctx, {
177
+ const result = await this.onSubmit(ctx, {
172
178
  raw_values: this.extractRawValues(ctx),
173
179
  messages: [],
174
180
  });
181
+ return this.onSuccess(
182
+ ctx,
183
+ {
184
+ raw_values: this.extractRawValues(ctx),
185
+ messages: [],
186
+ },
187
+ result
188
+ );
175
189
  } catch (e: unknown) {
176
190
  // eslint-disable-next-line no-console
177
191
  console.dir(e, { depth: 5 });
@@ -11,7 +11,7 @@ const FIELD_PREFIX_SEPARATOR = "___";
11
11
  export class Multiform extends MountableWithFields {
12
12
  controls = [];
13
13
  public name: string;
14
- public forms: Record<string, Form<Fields>>;
14
+ public forms: Record<string, Form<Fields, unknown>>;
15
15
 
16
16
  init(): void {
17
17
  super.init();
@@ -65,7 +65,9 @@ export class Multiform extends MountableWithFields {
65
65
  }
66
66
 
67
67
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- getSubformsToRender(ctx: Context): Array<readonly [string, Form<any>]> {
68
+ getSubformsToRender(
69
+ ctx: Context
70
+ ): Array<readonly [string, Form<any, unknown>]> {
69
71
  const requested_frame = ctx.headers["turbo-frame"];
70
72
  if (
71
73
  !is(
package/src/mount.ts CHANGED
@@ -33,8 +33,10 @@ export function mount(router: Router, url: string, mountable: Mountable): void {
33
33
 
34
34
  router.use(url, async (ctx, next) => {
35
35
  ctx.set("content-type", "text/html;charset=utf-8");
36
- if (!(await mountable.canAccess(ctx)).canAccess) {
37
- ctx.body = "no access";
36
+ const access_result = await mountable.canAccess(ctx);
37
+ if (!access_result.canAccess) {
38
+ ctx.status = 403;
39
+ ctx.body = access_result.message || "no access";
38
40
  return;
39
41
  }
40
42
  await next();
@@ -1,13 +1,14 @@
1
+ import { ShapeToType } from "@sealcode/ts-predicates";
1
2
  import { Context } from "koa";
2
3
  import { FlatTemplatable, tempstream } from "tempstream";
3
4
  import { FormControl } from "../forms/controls/form-control";
4
- import { FormField } from "../forms/fields/field";
5
+ import { FieldsToShape, FormField } from "../forms/fields/field";
5
6
  import { FormData, FormDataValue, FormMessage } from "../forms/form";
6
7
  import { Mountable } from "./mountable";
7
8
 
8
9
  export type PageErrorMessage = { type: "access" | "internal"; message: string };
9
10
 
10
- export type Fields = Record<string, FormField<unknown>>;
11
+ export type Fields = Record<string, FormField<boolean, unknown>>;
11
12
 
12
13
  export abstract class MountableWithFields<
13
14
  F extends Fields = Fields
@@ -109,4 +110,19 @@ export abstract class MountableWithFields<
109
110
  }
110
111
 
111
112
  abstract extractRawValues(ctx: Context): Record<string, FormDataValue>;
113
+
114
+ async getParsedValues(
115
+ ctx: Context
116
+ ): Promise<ShapeToType<FieldsToShape<F>>> {
117
+ const raw_values = this.extractRawValues(ctx);
118
+ const result: Record<string, unknown> = {};
119
+ const promises = Object.entries(this.fields).map(
120
+ async ([key, field]) => {
121
+ const { parsed } = await field.getValue(ctx, raw_values);
122
+ result[key] = parsed;
123
+ }
124
+ );
125
+ await Promise.all(promises);
126
+ return result as ShapeToType<FieldsToShape<F>>;
127
+ }
112
128
  }
@@ -17,7 +17,9 @@ const fields = {
17
17
  password: new Fields.SimpleFormField(true),
18
18
  };
19
19
 
20
- export default new (class ${action_name}Form extends Form<typeof fields> {
20
+ export const ${action_name}Shape = Fields.fieldsToShape(fields);
21
+
22
+ export default new (class ${action_name}Form extends Form<typeof fields, void> {
21
23
  defaultSuccessMessage = "Formularz wypełniony poprawnie";
22
24
  fields = fields;
23
25
 
@@ -14,7 +14,7 @@ const fields1 = {
14
14
  password: new Fields.SimpleFormField(true),
15
15
  };
16
16
 
17
- const form1 = new (class extends Form<typeof fields1> {
17
+ const form1 = new (class extends Form<typeof fields1, void> {
18
18
  defaultSuccessMessage = "Form1 done!";
19
19
  controls = [
20
20
  new Controls.FormHeader("form1"),
@@ -31,7 +31,7 @@ const fields2 = {
31
31
  surname: new Fields.SimpleFormField(true),
32
32
  };
33
33
 
34
- const form2 = new (class extends Form<Record<never, unknown>> {
34
+ const form2 = new (class extends Form<Record<never, unknown>, void> {
35
35
  defaultSuccessMessage = "Form2 done!";
36
36
  controls = [
37
37
  new Controls.FormHeader("form2"),