@sealcode/sealgen 0.15.33 → 0.15.35

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 (33) hide show
  1. package/@types/src/forms/controls/controls.d.ts +1 -0
  2. package/@types/src/forms/controls/table.d.ts +15 -0
  3. package/@types/src/forms/fields/fields.d.ts +1 -0
  4. package/@types/src/forms/fields/table.d.ts +12 -0
  5. package/@types/src/templates/shared/collection-create-form.d.ts +2 -0
  6. package/@types/src/templates/shared/collection-edit-form.d.ts +1 -0
  7. package/@types/src/templates/shared/collection-list.d.ts +1 -0
  8. package/@types/src/templates/shared/confirm-item-delete.d.ts +1 -0
  9. package/@types/src/templates/shared/item-delete.d.ts +1 -0
  10. package/@types/src/templates/shared/shared-crud-form-fields.d.ts +44 -0
  11. package/@types/src/utils/utils.d.ts +1 -0
  12. package/@types/src/utils/with-fallback.d.ts +2 -0
  13. package/lib/add-crud.js +131 -0
  14. package/lib/forms/controls/controls.js +1 -0
  15. package/lib/forms/controls/table.js +62 -0
  16. package/lib/forms/fields/fields.js +1 -0
  17. package/lib/forms/fields/table.js +58 -0
  18. package/lib/templates/shared/collection-create-form.js +146 -0
  19. package/lib/templates/shared/collection-edit-form.js +200 -0
  20. package/lib/templates/shared/collection-list.js +211 -0
  21. package/lib/templates/shared/confirm-item-delete.js +72 -0
  22. package/lib/templates/shared/item-delete.js +41 -0
  23. package/lib/templates/shared/shared-crud-form-fields.js +118 -0
  24. package/lib/utils/utils.js +1 -0
  25. package/lib/utils/with-fallback.js +21 -0
  26. package/lib/utils/write-file.js +11 -0
  27. package/package.json +1 -1
  28. package/src/forms/controls/controls.ts +1 -0
  29. package/src/forms/controls/table.ts +78 -0
  30. package/src/forms/fields/fields.ts +1 -0
  31. package/src/forms/fields/table.ts +72 -0
  32. package/src/utils/utils.ts +1 -0
  33. package/src/utils/with-fallback.ts +47 -0
@@ -28,3 +28,4 @@ export * from "./multiple-files.js";
28
28
  export * from "./number.js";
29
29
  export * from "./multiple-photos.js";
30
30
  export * from "./array.js";
31
+ export * from "./table.js";
@@ -0,0 +1,15 @@
1
+ import { FlatTemplatable } from "tempstream";
2
+ import { FormField } from "../fields/field.js";
3
+ import { Table as TableField } from "../fields/table.js";
4
+ import { FormControl, FormControlContext } from "./form-control.js";
5
+ export declare class Table<F extends Record<string, FormField>> extends FormControl {
6
+ table_field: TableField<F>;
7
+ render_fields: {
8
+ [field_name in keyof F]: (fctx: FormControlContext, name: string, value: string) => string;
9
+ };
10
+ role: "input";
11
+ constructor(table_field: TableField<F>, render_fields: {
12
+ [field_name in keyof F]: (fctx: FormControlContext, name: string, value: string) => string;
13
+ });
14
+ render(fctx: FormControlContext): Promise<FlatTemplatable>;
15
+ }
@@ -13,3 +13,4 @@ export * from "./file.js";
13
13
  export * from "./multiple-files.js";
14
14
  export * from "./reverse-single-reference.js";
15
15
  export * from "./structured-array.js";
16
+ export * from "./table.js";
@@ -0,0 +1,12 @@
1
+ import { Context } from "koa";
2
+ import { FormDataValue } from "../form-types.js";
3
+ import { FieldParseResult, FormField } from "./field.js";
4
+ export type TableFieldParsed<F extends Record<string, FormField>> = {
5
+ [field_name in keyof F]: unknown;
6
+ }[];
7
+ export declare class Table<F extends Record<string, FormField>> extends FormField<true, TableFieldParsed<F>> {
8
+ columns: F;
9
+ constructor(columns: F);
10
+ getEmptyValue(): never[];
11
+ parse(ctx: Context, raw_value: FormDataValue): Promise<FieldParseResult<TableFieldParsed<F>>>;
12
+ }
@@ -1 +1,3 @@
1
+ import { ExtractedFieldInfo } from "../../utils/extract-fields-from-collection.js";
2
+ export declare function renderFieldForSealious({ name, type, is_required, is_readonly, }: ExtractedFieldInfo): string;
1
3
  export declare function createItemFormTemplate(create_action_name: string, newfilefullpath: string, collection_name: string, list_action: string): Promise<string>;
@@ -0,0 +1 @@
1
+ export declare function editItemFormTemplate(edit_action_name: string, newfilefullpath: string, collection_name: string, list_action: string): Promise<string>;
@@ -10,6 +10,7 @@ declare const default_hooks: {
10
10
  pre_header_html: string;
11
11
  post_header_html: string;
12
12
  post_import_js: string;
13
+ render_item: (collection_name: string) => string;
13
14
  };
14
15
  export declare function collectionListTemplate(collection_name: string, action_name: string, newfilefullpath: string, _hooks?: Partial<typeof default_hooks>): Promise<string>;
15
16
  export {};
@@ -0,0 +1 @@
1
+ export declare function askItemDeleteTemplate(ask_delete_action_name: string, collection_name: string, newfilefullpath: string, list_action_name: string, delete_action_name: string): Promise<string>;
@@ -0,0 +1 @@
1
+ export declare function itemDeleteTemplate(delete_action_name: string, collection_name: string, newfilefullpath: string, list_action_name: string): Promise<string>;
@@ -1 +1,45 @@
1
+ import { ExtractedFieldInfo } from "../../utils/extract-fields-from-collection.js";
2
+ type FieldHandlerResult = {
3
+ imports: Partial<{
4
+ shared: string;
5
+ edit: string;
6
+ }>;
7
+ field: string;
8
+ controls: string;
9
+ is_required: boolean;
10
+ is_required_expr: string;
11
+ fields_var: string;
12
+ top_level_shared: string;
13
+ name: string;
14
+ initial_value: Partial<{
15
+ main_value: string;
16
+ parsed_value: string;
17
+ fallback_value: string;
18
+ }> | string;
19
+ post_edit: string;
20
+ };
21
+ type FieldHandler<Defaults extends Partial<FieldHandlerResult>, Result extends Partial<FieldHandlerResult>> = (collection_name: string, field: ExtractedFieldInfo, importPath: (path: string) => string, defaults: Defaults) => Promise<Result>;
22
+ export declare const field_handlers: Record<string, FieldHandler<FieldHandlerResult, Partial<FieldHandlerResult>> | undefined> & {
23
+ default: FieldHandler<Record<string, never>, FieldHandlerResult>;
24
+ };
25
+ export declare function getFieldHandler(collection_name: string, field: ExtractedFieldInfo, importPath: (path: string) => string): Promise<{
26
+ imports: Partial<{
27
+ shared: string;
28
+ edit: string;
29
+ }>;
30
+ field: string;
31
+ controls: string;
32
+ is_required: boolean;
33
+ is_required_expr: string;
34
+ fields_var: string;
35
+ top_level_shared: string;
36
+ name: string;
37
+ initial_value: string | Partial<{
38
+ main_value: string;
39
+ parsed_value: string;
40
+ fallback_value: string;
41
+ }>;
42
+ post_edit: string;
43
+ }>;
1
44
  export declare function sharedCrudFormFields(collection_name: string, newfilefullpath: string): Promise<string>;
45
+ export {};
@@ -14,3 +14,4 @@ export * from "./make-slug.js";
14
14
  export * from "./render-attributes.js";
15
15
  export * from "./format-external-url.js";
16
16
  export * from "./base64.js";
17
+ export * from "./with-fallback.js";
@@ -0,0 +1,2 @@
1
+ export declare function withFallback<T1, T2 extends NonNullable<unknown>>(value1: T1, value2: T2): NonNullable<T1> | T2;
2
+ export declare function withFallback<T1, T2, T3 extends NonNullable<unknown>>(value1: T1, value2: T2, value3?: T3): NonNullable<T1> | NonNullable<T2> | T3;
@@ -0,0 +1,131 @@
1
+ import _locreq from "locreq";
2
+ import { escape_url_params } from "./utils/escape-url-params.js";
3
+ import prompts from "prompts";
4
+ import { toKebabCase, toPascalCase } from "js-convert-case";
5
+ import { listCollections } from "./utils/list-collections.js";
6
+ import { collectionListTemplate } from "./templates/shared/collection-list.js";
7
+ import { writeFile } from "./utils/write-file.js";
8
+ import { importPath } from "./utils/import-path.js";
9
+ import { sharedCrudFormFields } from "./templates/shared/shared-crud-form-fields.js";
10
+ import { generateRoutes } from "./generate-routes.js";
11
+ import { createItemFormTemplate } from "./templates/shared/collection-create-form.js";
12
+ import { editItemFormTemplate } from "./templates/shared/collection-edit-form.js";
13
+ import { askItemDeleteTemplate } from "./templates/shared/confirm-item-delete.js";
14
+ import { itemDeleteTemplate } from "./templates/shared/item-delete.js";
15
+ async function addCRUD(params, app_directory = process.cwd()) {
16
+ const target_locreq = _locreq(app_directory);
17
+ prompts.override(params);
18
+ const response = await prompts([
19
+ {
20
+ type: "autocomplete",
21
+ name: "collection",
22
+ message: "Which sealious collection do you like to add CRUD forms for?",
23
+ choices: (await listCollections()).map((collection) => ({
24
+ title: collection,
25
+ value: collection
26
+ }))
27
+ },
28
+ {
29
+ type: "text",
30
+ name: "url",
31
+ message: "Enter a full absolute path for the new route (for example: /admin/users/:id/edit): ",
32
+ validate: (s) => s.trim()[0] == "/" ? true : "Should start with a '/'",
33
+ initial: function(_, { collection }) {
34
+ return "/" + toKebabCase(collection);
35
+ }
36
+ }
37
+ ]);
38
+ const collection_name = response.collection;
39
+ const url = response.url;
40
+ const list_action_name = toPascalCase(collection_name) + "List";
41
+ const create_action_name = toPascalCase(collection_name) + "Create";
42
+ const edit_action_name = toPascalCase(collection_name) + "Edit";
43
+ const delete_ask_action_name = toPascalCase(collection_name) + "DeleteAsk";
44
+ const delete_action_name = toPascalCase(collection_name) + "Delete";
45
+ const list_path = target_locreq.resolve(
46
+ `src/back/routes/${escape_url_params(url)}.list.tsx`
47
+ );
48
+ const list_content = await collectionListTemplate(
49
+ collection_name,
50
+ list_action_name,
51
+ list_path,
52
+ {
53
+ post_import_js: `import { ${create_action_name}URL, ${edit_action_name}URL, ${delete_ask_action_name}URL } from "${importPath(
54
+ list_path,
55
+ "src/back/routes/urls.ts"
56
+ )}";`,
57
+ post_header_html: `<a href={${create_action_name}URL}> Create </a>`,
58
+ render_item: (collection_name2) => ` async renderItem(ctx: Context, item: CollectionItem<typeof ${toPascalCase(
59
+ collection_name2
60
+ )}>) {
61
+ return <tr>
62
+ {displayFields.map(({ field, format }) => {
63
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
64
+ const value = item.get(field as any);
65
+ return <td>{format ? format(value, item) : value}</td>;
66
+ })}
67
+ <td><div class="sealious-list__actions">
68
+ <a href={${edit_action_name}URL(item.id)}>Edit</a>
69
+ <a href={${delete_ask_action_name}URL(item.id)}>Delete</a>
70
+ </div>
71
+ </td>
72
+ </tr>;
73
+ }`
74
+ }
75
+ );
76
+ await writeFile(list_path, list_content);
77
+ const fields_path = target_locreq.resolve(
78
+ `src/back/routes/${escape_url_params(url)}/shared.ts`
79
+ );
80
+ const fields_content = await sharedCrudFormFields(
81
+ collection_name,
82
+ fields_path
83
+ );
84
+ await writeFile(fields_path, fields_content);
85
+ const create_path = target_locreq.resolve(
86
+ `src/back/routes/${escape_url_params(url)}/create.form.ts`
87
+ );
88
+ const create_content = await createItemFormTemplate(
89
+ create_action_name,
90
+ create_path,
91
+ collection_name,
92
+ list_action_name
93
+ );
94
+ await writeFile(create_path, create_content);
95
+ const edit_path = target_locreq.resolve(
96
+ `src/back/routes/${escape_url_params(url)}/[id]/edit.form.ts`
97
+ );
98
+ const edit_content = await editItemFormTemplate(
99
+ edit_action_name,
100
+ edit_path,
101
+ collection_name,
102
+ list_action_name
103
+ );
104
+ await writeFile(edit_path, edit_content);
105
+ const delete_ask_path = target_locreq.resolve(
106
+ `src/back/routes/${escape_url_params(url)}/[id]/delete-ask.page.tsx`
107
+ );
108
+ const delete_ask_content = await askItemDeleteTemplate(
109
+ delete_ask_action_name,
110
+ collection_name,
111
+ delete_ask_path,
112
+ list_action_name,
113
+ delete_action_name
114
+ );
115
+ await writeFile(delete_ask_path, delete_ask_content);
116
+ const delete_path = target_locreq.resolve(
117
+ `src/back/routes/${escape_url_params(url)}/[id]/delete.page.tsx`
118
+ );
119
+ const delete_content = await itemDeleteTemplate(
120
+ delete_action_name,
121
+ collection_name,
122
+ delete_ask_path,
123
+ list_action_name
124
+ );
125
+ await writeFile(delete_path, delete_content);
126
+ await generateRoutes();
127
+ }
128
+ export {
129
+ addCRUD
130
+ };
131
+ //# sourceMappingURL=add-crud.js.map
@@ -28,6 +28,7 @@ export * from "./multiple-files.js";
28
28
  export * from "./number.js";
29
29
  export * from "./multiple-photos.js";
30
30
  export * from "./array.js";
31
+ export * from "./table.js";
31
32
  export {
32
33
  CheckboxedListInput,
33
34
  EditableCollectionSubset,
@@ -0,0 +1,62 @@
1
+ import { FormControl } from "./form-control.js";
2
+ class Table extends FormControl {
3
+ constructor(table_field, render_fields) {
4
+ super();
5
+ this.table_field = table_field;
6
+ this.render_fields = render_fields;
7
+ this.role = "input";
8
+ }
9
+ async render(fctx) {
10
+ const { parsed: rows } = await this.table_field.getValue(
11
+ fctx.ctx,
12
+ fctx.data
13
+ );
14
+ if (!rows) {
15
+ return "";
16
+ }
17
+ return (
18
+ /* HTML */
19
+ `<div
20
+ class="${[
21
+ "form-input__wrapper",
22
+ "form-input__wrapper--type--table",
23
+ "form-input__wrapper--options-count--" + rows.length.toString(),
24
+ ...rows.length >= 5 ? ["form-input__wrapper--options-count--5-or-more"] : [],
25
+ ...rows.length >= 10 ? ["form-input__wrapper--options-count--10-or-more"] : [],
26
+ ...rows.length >= 15 ? ["form-input__wrapper--options-count--15-or-more"] : [],
27
+ ...rows.length >= 20 ? ["form-input__wrapper--options-count--20-or-more"] : []
28
+ ].join(" ")}"
29
+ >
30
+ <div class="table__wrapper">
31
+ <table>
32
+ <tbody>
33
+ <tr>
34
+ ${rows?.map((row, row_index) => {
35
+ return (
36
+ /* HTML */
37
+ `<tr>
38
+ ${Object.entries(
39
+ this.table_field.columns
40
+ ).map(([key]) => {
41
+ return `<td>${this.render_fields[key](
42
+ fctx,
43
+ `${this.table_field.name}[${row_index}][${key}]`,
44
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
45
+ row[key]
46
+ )}</td>`;
47
+ }).join("")}
48
+ </tr>`
49
+ );
50
+ }).join("")}
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ </div>`
56
+ );
57
+ }
58
+ }
59
+ export {
60
+ Table
61
+ };
62
+ //# sourceMappingURL=table.js.map
@@ -13,4 +13,5 @@ export * from "./file.js";
13
13
  export * from "./multiple-files.js";
14
14
  export * from "./reverse-single-reference.js";
15
15
  export * from "./structured-array.js";
16
+ export * from "./table.js";
16
17
  //# sourceMappingURL=fields.js.map
@@ -0,0 +1,58 @@
1
+ import { is, predicates } from "@sealcode/ts-predicates";
2
+ import { FormField } from "./field.js";
3
+ class Table extends FormField {
4
+ constructor(columns) {
5
+ super(true);
6
+ this.columns = columns;
7
+ }
8
+ getEmptyValue() {
9
+ return [];
10
+ }
11
+ async parse(ctx, raw_value) {
12
+ if (!is(raw_value, predicates.object)) {
13
+ return {
14
+ parsable: false,
15
+ error: "Expected an object",
16
+ parsed_value: null
17
+ };
18
+ }
19
+ const result = [];
20
+ for (const row_number in raw_value) {
21
+ const row_data = raw_value[row_number];
22
+ const parsed_row = {};
23
+ if (!is(row_data, predicates.object)) {
24
+ throw new Error(`Expected object, got ${String(row_data)}`);
25
+ }
26
+ for (const key in row_data) {
27
+ const field = this.columns[key];
28
+ if (field == void 0) {
29
+ return {
30
+ parsable: false,
31
+ error: `Unknown column: ${key}`,
32
+ parsed_value: null
33
+ };
34
+ }
35
+ const parse_result = await field.parse(
36
+ ctx,
37
+ row_data[key]
38
+ );
39
+ if (!parse_result.parsable) {
40
+ console.debug("table.ts:51");
41
+ return {
42
+ parsable: false,
43
+ error: `Invalid value for row ${row_number}.${key}: ${parse_result.error}`,
44
+ parsed_value: null
45
+ };
46
+ }
47
+ parsed_row[key] = parse_result.parsed_value;
48
+ }
49
+ result.push(parsed_row);
50
+ }
51
+ const ret = { parsed_value: result, parsable: true, error: null };
52
+ return ret;
53
+ }
54
+ }
55
+ export {
56
+ Table
57
+ };
58
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1,146 @@
1
+ import { toCamelCase, toKebabCase, toPascalCase } from "js-convert-case";
2
+ import { formTemplate } from "../form.js";
3
+ import { curryImportPath } from "../../utils/import-path.js";
4
+ import extract_fields_from_collection from "../../utils/extract-fields-from-collection.js";
5
+ function renderFieldForSealious({
6
+ name,
7
+ type,
8
+ is_required,
9
+ is_readonly
10
+ }) {
11
+ if (type == "deep-reverse-single-reference" || type == "jdd" || is_readonly) {
12
+ return "";
13
+ }
14
+ let fallback_value = `""`;
15
+ let main_value = `data.${name}`;
16
+ if (type == "image" || type == "file") {
17
+ fallback_value = `null`;
18
+ if (is_required) {
19
+ main_value = toCamelCase("prepared-" + name);
20
+ }
21
+ }
22
+ if (type == "boolean") {
23
+ main_value = `!!data.${name}`;
24
+ fallback_value = "false";
25
+ }
26
+ if (type == "date") {
27
+ fallback_value == `undefined`;
28
+ }
29
+ if (is_required) {
30
+ return `${name}: ${main_value}`;
31
+ } else {
32
+ return `${name}: ${main_value} || ${fallback_value}`;
33
+ }
34
+ }
35
+ function renderFieldPreCreateAction({ name, type }) {
36
+ if (type == "image" || type == "file") {
37
+ const tempname = toCamelCase("prepared-" + name);
38
+ return `const ${tempname} = data.${name}.new || data.${name}.old;
39
+ if(!${tempname}){
40
+ throw new Error("Missing field: ${name}")
41
+ }`;
42
+ }
43
+ return "";
44
+ }
45
+ function renderFieldPostCreateAction({
46
+ name,
47
+ type,
48
+ referencing_collection,
49
+ referencing_field,
50
+ intermediary_field_that_points_there
51
+ }) {
52
+ if (type == "deep-reverse-single-reference" && referencing_collection && referencing_field && intermediary_field_that_points_there) {
53
+ return ` await (async function(){
54
+ // We iterate over all selected values and insert appropriate entries for the ${name} relation
55
+ const ids = Object.entries(
56
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
57
+ data.${name} as unknown as Record<string, string>
58
+ )
59
+ .filter(([, value]) => value === "on")
60
+ .map(([key]) => key);
61
+
62
+ const promises = ids.map(async (id) => {
63
+ await ctx.$app.collections["${referencing_collection}"].create(
64
+ new ctx.$app.SuperContext(),
65
+ {
66
+ "${referencing_field}": created.id,
67
+ "${intermediary_field_that_points_there}": id,
68
+ }
69
+ );
70
+ });
71
+
72
+ await Promise.all(promises);
73
+ })()`;
74
+ }
75
+ return "";
76
+ }
77
+ async function createItemFormTemplate(create_action_name, newfilefullpath, collection_name, list_action) {
78
+ const collection_fields = await extract_fields_from_collection(
79
+ collection_name
80
+ );
81
+ const importPath = curryImportPath(newfilefullpath);
82
+ return formTemplate(create_action_name, newfilefullpath, {
83
+ postimport: `import {${toPascalCase(
84
+ collection_name
85
+ )}FormFields, ${toPascalCase(
86
+ collection_name
87
+ )}FormControls }from "./shared.js";
88
+
89
+ import { ${toPascalCase(collection_name)} } from "${importPath(
90
+ "src/back/collections/collections.js"
91
+ )}";
92
+
93
+ import { ${list_action}URL } from "${importPath("src/back/routes/urls.js")}";
94
+
95
+ import {tempstream} from "tempstream";
96
+ `,
97
+ fields: `...${toPascalCase(collection_name)}FormFields`,
98
+ controls: `
99
+ new Controls.FormHeader("Create ${toPascalCase(
100
+ collection_name
101
+ )}"),
102
+ ...${toPascalCase(collection_name)}FormControls`,
103
+ can_access: `canAccess = async (ctx: Context) => {
104
+ const policy = ${toPascalCase(collection_name)}.getPolicy("create");
105
+ const response = await policy.check(ctx.$context);
106
+ return { canAccess: response?.allowed || false, message: response?.reason || "" };
107
+ };`,
108
+ onsubmit: `
109
+ async onSubmit(ctx: Context) {
110
+ const data = await this.getParsedValues(ctx);
111
+ if (!data) {
112
+ throw new Error("Error when parsing the form values");
113
+ }
114
+
115
+ ${collection_fields.map((field) => renderFieldPreCreateAction(field)).filter((e) => e !== "").join("\n")}
116
+
117
+ const created = await ctx.$app.collections["${toKebabCase(
118
+ collection_name
119
+ )}"].create(
120
+ ctx.$context,
121
+ {
122
+ ${collection_fields.map((field) => renderFieldForSealious(field)).filter((e) => e != "").join(",\n")}
123
+ }
124
+ );
125
+ ${collection_fields.map((field) => renderFieldPostCreateAction(field)).filter((e) => e != "").join("\n")}
126
+ }
127
+ `,
128
+ render: ` async render(ctx: Context, data: FormData, show_field_errors: boolean) {
129
+ return html(
130
+ ctx,
131
+ "AddBlogArticle",
132
+ tempstream/* HTML */ \` <div class="add-opinion-form">
133
+ <a class="" href="\${${list_action}URL}"
134
+ >\u2190 Back to ${collection_name} list</a
135
+ >
136
+ \${await super.render(ctx, data, show_field_errors)}
137
+ </div>\`
138
+ );
139
+ }`
140
+ });
141
+ }
142
+ export {
143
+ createItemFormTemplate,
144
+ renderFieldForSealious
145
+ };
146
+ //# sourceMappingURL=collection-create-form.js.map