Page MenuHomeSealhub

No OneTemporary

diff --git a/src/cli.ts b/src/cli.ts
index 7093f80..2edf245 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,45 +1,47 @@
#!/usr/bin/env node
+import yargs from "yargs/yargs";
import { addCollection } from "./add-collection.js";
+import { addCRUD } from "./add-crud.js";
import { addJDDComponent } from "./add-jdd-component.js";
import { addRoute } from "./add-route.js";
+import { addVerboseCRUD } from "./add-verbose-crud.js";
import { buildProject } from "./build.js";
import { generateCollections } from "./generate-collections.js";
import { generateComponents } from "./generate-components.js";
import { generateRoutes } from "./generate-routes.js";
-import { makeEnv } from "./make-env.js";
-import yargs from "yargs/yargs";
import { getFonts } from "./get-fonts.js";
-import { addVerboseCRUD } from "./add-verbose-crud.js";
-import { addCRUD } from "./add-crud.js";
+import { makeEnv } from "./make-env.js";
+import { crudFieldSnippets } from "./crud-field-snippets.js";
const actions: Record<
string,
(args: Record<string, string | boolean>) => Promise<void> | undefined
> = {
"add-collection": addCollection,
"add-route": addRoute,
"add-verbose-crud": addVerboseCRUD,
"add-crud": addCRUD,
"generate-collections": generateCollections,
"generate-components": generateComponents,
"generate-routes": generateRoutes,
build: buildProject,
default: async function () {
console.log("Usage: `npx sealgen <action>`");
console.log(
`Available actions: ${Object.keys(actions)
.filter((e) => e != "default")
.join(", ")}`
);
},
"make-env": makeEnv,
"add-component": addJDDComponent,
"get-fonts": getFonts,
+ "crud-field-snippets": crudFieldSnippets,
};
void (async function () {
const action = process.argv.at(2);
const fn = actions[action || "default"] || actions.default;
const args = yargs(process.argv).argv as Record<string, string | boolean>;
await fn(args);
})();
diff --git a/src/crud-field-snippets.ts b/src/crud-field-snippets.ts
new file mode 100644
index 0000000..6358347
--- /dev/null
+++ b/src/crud-field-snippets.ts
@@ -0,0 +1,57 @@
+import _locreq from "locreq";
+import prompts from "prompts";
+import { getFieldHandler } from "./templates/shared/shared-crud-form-fields.js";
+import extract_fields_from_collection from "./utils/extract-fields-from-collection.js";
+import { listCollections } from "./utils/list-collections.js";
+import { formatWithPrettier } from "./utils/prettier.js";
+
+export async function crudFieldSnippets(
+ params: Partial<{ [key in "collection" | "url"]: string }>,
+ app_directory: string = process.cwd()
+) {
+ const target_locreq = _locreq(app_directory);
+ prompts.override(params);
+ const { collection_name } = (await prompts([
+ {
+ type: "autocomplete",
+ name: "collection_name",
+ message:
+ "Which sealious collection do you like to show a CRUD field config for?",
+ choices: (
+ await listCollections()
+ ).map((collection) => ({
+ title: collection,
+ value: collection,
+ })),
+ },
+ ])) as { collection_name: string };
+ const all_fields = await extract_fields_from_collection(collection_name);
+ const { field_name } = (await prompts([
+ {
+ type: "autocomplete",
+ name: "field_name",
+ message: `Which field do you want to show CRUD config for??`,
+ choices: all_fields.map((field) => ({
+ title: field.name,
+ value: field.name,
+ })),
+ },
+ ])) as { field_name: string };
+
+ const handler = await getFieldHandler(
+ collection_name,
+ all_fields.find((e) => e.name == field_name)!,
+ (p) => p
+ );
+
+ console.log(`
+
+### Field in shared.ts
+
+${handler.field}
+
+### Control in shared.ts
+
+${await formatWithPrettier(handler.controls)}
+`);
+}
diff --git a/src/find-css-includes.ts b/src/find-css-includes.ts
index 8112276..8335111 100644
--- a/src/find-css-includes.ts
+++ b/src/find-css-includes.ts
@@ -1,6 +1,3 @@
-import { promises as fs } from "fs";
import _locreq from "locreq";
-import { relative } from "path";
-import { walkDir } from "./utils/walk.js";
const target_locreq = _locreq(process.cwd());
diff --git a/src/page/stateful-page.ts b/src/page/stateful-page.ts
index b4eb7c7..9fc49c7 100644
--- a/src/page/stateful-page.ts
+++ b/src/page/stateful-page.ts
@@ -1,328 +1,330 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Router from "@koa/router";
import { predicates, hasShape, is } from "@sealcode/ts-predicates";
import deepmerge, { ArrayMergeOptions } from "deepmerge";
import { Context } from "koa";
import { Templatable, tempstream } from "tempstream";
import { from_base64, to_base64 } from "../utils/base64.js";
import { Mountable } from "./mountable.js";
import { isPlainObject } from "is-what";
export type StatefulPageActionDescription<ActionName> =
| {
action: ActionName;
label?: string;
content?: string;
disabled?: boolean;
}
| ActionName;
export type StateAndMetadata<State, Actions> =
| {
state: State;
inputs: Record<string, string>;
action: keyof Actions;
action_args: string;
}
| {
state: State;
inputs: Record<string, string>;
action: null;
action_args: null;
$: Record<string, unknown>;
};
export type StatefulPageActionArgument<
State extends Record<string, unknown>,
Args extends unknown[] = unknown[]
> = {
ctx: Context;
state: State;
inputs: Record<string, string>;
args: Args;
page: StatefulPage<any, any>;
};
export type StatefulPageAction<
State extends Record<string, unknown>,
Args extends unknown[] = unknown[]
> = (obj: StatefulPageActionArgument<State, Args>) => State | Promise<State>;
export type ExtractStatefulPageActionArgs<X> = X extends StatefulPageAction<
any,
infer Args
>
? Args
: never;
export abstract class StatefulPage<
State extends Record<string, unknown>,
Actions extends Record<string, StatefulPageAction<State>>
> extends Mountable {
abstract actions: Actions;
abstract getInitialState(ctx: Context): State | Promise<State>;
abstract render(
ctx: Context,
state: State,
inputs: Record<string, string>
): Templatable | Promise<Templatable>;
- async canAccess(): Promise<{ canAccess: boolean; message: string }> {
+ async canAccess(
+ _ctx: Context
+ ): Promise<{ canAccess: boolean; message: string }> {
return <const>{ canAccess: true, message: "" };
}
constructor() {
super();
const original_render = this.render.bind(this) as typeof this.render;
this.render = async (
ctx: Context,
state: State,
inputs: Record<string, string>
) => {
return this.wrapInLayout(
ctx,
await this.wrapInForm(
ctx,
state,
await original_render(ctx, state, inputs)
),
state
);
};
}
abstract wrapInLayout(
ctx: Context,
content: Templatable,
state: State
): Templatable;
async wrapInForm(
context: Context,
state: State,
content: Templatable
): Promise<Templatable> {
return tempstream/* HTML */ `<form
action="./"
method="POST"
enctype="multipart/form-data"
>
<input
name="state"
type="hidden"
value="${to_base64(await this.serializeState(context, state))}"
/>
${content}
</form>`;
}
extractActionAndLabel<ActionName extends keyof Actions>(
action_description: StatefulPageActionDescription<ActionName>
): { action: string; label: string; content: string; disabled: boolean } {
let label, action, content: string;
let disabled: boolean;
if (is(action_description, predicates.object)) {
action = action_description.action.toString();
label = action_description.label || action;
content = action_description.content || label;
disabled = action_description.disabled || false;
} else {
action = action_description.toString();
label = action;
content = label;
disabled = false;
}
return { action, label, content, disabled };
}
makeActionURL<ActionName extends keyof Actions>(
action_description: StatefulPageActionDescription<ActionName>,
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
) {
const { action } = this.extractActionAndLabel(action_description);
return `./?action=${action}&action_args=${encodeURIComponent(
// encoding as URI Component because sometimes it can contain a "+" which is treated as a space
to_base64(JSON.stringify(args))
)}`;
}
makeActionButton<ActionName extends keyof Actions>(
_state: State,
action_description: StatefulPageActionDescription<ActionName>,
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
) {
const { label, content, disabled } =
this.extractActionAndLabel(action_description);
return /* HTML */ `
<button
type="submit"
formaction="${this.makeActionURL(action_description, ...args)}"
title="${label}"
${disabled ? "disabled" : ""}
>
${content}
</button>
`;
}
makeActionCallback<ActionName extends keyof Actions>(
action_description:
| {
action: ActionName;
label?: string;
}
| ActionName,
...args: ExtractStatefulPageActionArgs<Actions[ActionName]>
) {
return `(()=>{const form = this.closest('form'); form.action='${this.makeActionURL(
action_description,
...args
)}'; form.requestSubmit()})()`;
}
rerender() {
return "this.closest('form').requestSubmit()";
}
async preprocessState(values: State): Promise<State> {
return values;
}
async preprocessOverrides(
_context: Context,
_state: State,
values: Record<string, unknown>
): Promise<Record<string, unknown>> {
return values;
}
async serializeState(_context: Context, state: State): Promise<string> {
return JSON.stringify(state);
}
async deserializeState(_context: Context, s: string): Promise<State> {
const deserialized = JSON.parse(s) as unknown;
return deserialized as State;
}
async extractState(
ctx: Context
): Promise<StateAndMetadata<State, Actions>> {
if (
!hasShape(
{
action: predicates.maybe(predicates.string),
state: predicates.string,
action_args: predicates.maybe(predicates.string),
$: predicates.maybe(predicates.object),
},
ctx.$body
)
) {
console.error("Wrong data: ", ctx.$body);
throw new Error("wrong formdata shape");
}
const inputs = Object.fromEntries(
Object.entries(ctx.$body).filter(
([key]) => !["action", "state", "args", "$"].includes(key)
)
) as Record<string, string>;
// the "$" key is parsed as dot notation and overrides the state
const original_state_string = ctx.$body.state;
const original_state = await this.deserializeState(
ctx,
typeof original_state_string == "string"
? from_base64(original_state_string)
: "{}"
);
const $body = ctx.$body;
let state_overrides = $body.$ || {};
state_overrides = await this.preprocessOverrides(
ctx,
original_state,
state_overrides
);
let modified_state = deepmerge(original_state, state_overrides, {
isMergeableObject: (v) => isPlainObject(v) || Array.isArray(v),
arrayMerge: (
target: any[],
source: any[],
options: ArrayMergeOptions
) => {
// https://github.com/TehShrike/deepmerge?tab=readme-ov-file#arraymerge-example-combine-arrays
const destination = target.slice();
/* eslint-disable @typescript-eslint/no-unsafe-argument */
source.forEach((item, index) => {
if (typeof destination[index] === "undefined") {
destination[index] =
options.cloneUnlessOtherwiseSpecified(
item,
options
);
} else if (options.isMergeableObject(item)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
destination[index] = deepmerge(
target[index],
item,
options
);
} else if (target.indexOf(item) === -1) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
destination[index] = item;
}
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return destination;
},
}) as State;
// giving extending classes a change to modify the state before furhter processing
modified_state = await this.preprocessState(modified_state);
if (ctx.$body.action && ctx.$body.action_args) {
return {
state: modified_state,
action: ctx.$body.action,
inputs,
action_args: ctx.$body.action_args,
};
} else {
return {
state: modified_state,
action: null,
inputs,
action_args: null,
$: ctx.$body.$ || {},
};
}
}
mount(router: Router, path: string) {
router.get(path, async (ctx) => {
ctx.body = this.render(ctx, await this.getInitialState(ctx), {});
});
router.post(path, async (ctx) => {
const { action, state, inputs, action_args } =
await this.extractState(ctx);
if (action) {
const new_state = await this.actions[action]({
ctx,
state,
inputs,
args: JSON.parse(
from_base64(action_args as string)
) as unknown[],
page: this,
});
ctx.body = this.render(ctx, new_state, inputs);
} else {
ctx.body = this.render(ctx, state, inputs);
}
ctx.status = 422;
});
}
}
diff --git a/src/templates/shared/handlers/default.ts b/src/templates/shared/handlers/default.ts
index 5a1eddc..687278c 100644
--- a/src/templates/shared/handlers/default.ts
+++ b/src/templates/shared/handlers/default.ts
@@ -1,63 +1,64 @@
import { toPascalCase } from "js-convert-case";
import {
FieldHandler,
FieldHandlerResult,
} from "../shared-crud-form-fields.js";
export const default_handler: FieldHandler<
Record<string, never>,
FieldHandlerResult
> = async (
collection_name,
{ name, is_readonly, is_required },
_,
__,
+ ___,
fields_var = `${toPascalCase(collection_name)}FormFields`
) => {
const required_s = `${toPascalCase(
collection_name
)}.fields.${name}.required`;
const fallback_value_initial = `""`;
const fallback_value_sealious = `""`;
const main_value_initial = `item.get("${name}")`;
const main_value_sealious = `data["${name}"]`;
const parsed_value = main_value_initial;
return {
is_required,
name,
post_create: "",
imports: { shared: "", edit: "" },
hide_field: is_readonly,
field: `${name}: new Fields.CollectionField(${required_s}, ${toPascalCase(
collection_name
)}.fields.${name})`,
hide_control: is_readonly,
controls: `new Controls.SimpleInput(${fields_var}.${name}, { label: "${name}"})`,
is_required_expr: `${toPascalCase(
collection_name
)}.fields.${name}.required`,
fields_var,
top_level_shared: "",
hide_initial_value_edit: is_readonly,
initial_value_edit: {
fallback_value: fallback_value_initial,
main_value: main_value_initial,
parsed_value,
},
hide_initial_value_create: is_readonly,
initial_value_create: "",
pre_edit: "",
post_edit: "",
pre_create: "",
main_value_sealious,
fallback_value_sealious,
sealious_value: (result: FieldHandlerResult) => {
return result.is_required
? `${result.name}: ${result.main_value_sealious}`
: `${result.name}: ${result.main_value_sealious} != null ? ${result.main_value_sealious} : ${result.fallback_value_sealious}`;
},
hide_sealious_value: is_readonly,
};
};
diff --git a/src/templates/shared/shared-crud-form-fields.ts b/src/templates/shared/shared-crud-form-fields.ts
index 251692f..b7c421a 100644
--- a/src/templates/shared/shared-crud-form-fields.ts
+++ b/src/templates/shared/shared-crud-form-fields.ts
@@ -1,185 +1,193 @@
import { toPascalCase } from "js-convert-case";
import { extractCollectionClassname } from "../../generate-collections.js";
import extract_fields_from_collection, {
ExtractedFieldInfo,
} from "../../utils/extract-fields-from-collection.js";
import _locreq from "locreq";
import { curryImportPath } from "../../utils/import-path.js";
import { formatWithPrettier } from "../../utils/prettier.js";
import { default_handler } from "./handlers/default.js";
import { enum_handler } from "./handlers/enum.js";
import { image_handler } from "./handlers/image.js";
import { file_handler } from "./handlers/file.js";
import { int_handler } from "./handlers/int.js";
import { boolean_handler } from "./handlers/boolean.js";
import { jdd_handler } from "./handlers/jdd.js";
import { date_handler } from "./handlers/date.js";
import { deep_reverse_single_reference_handler } from "./handlers/deep-reverse-single-reference.js";
import { structured_array_handler } from "./handlers/structured-array.js";
import { single_reference_handler } from "./handlers/single-reference.js";
const target_locreq = _locreq(process.cwd());
export type FieldHandlerResult = {
imports: Partial<{ shared: string; edit: string }>;
hide_field: boolean;
field: string;
hide_control: boolean;
controls: string;
is_required: boolean;
is_required_expr: string;
fields_var: string;
top_level_shared: string;
name: string;
hide_initial_value_edit: boolean;
initial_value_edit:
| Partial<{
main_value: string;
parsed_value: string;
fallback_value: string;
}>
| string;
hide_initial_value_create: boolean;
initial_value_create: string;
pre_edit: string;
post_edit: string;
pre_create: string;
post_create: string;
sealious_value: (
result: Exclude<FieldHandlerResult, "sealious_value">
) => string;
hide_sealious_value: boolean;
fallback_value_sealious: string;
main_value_sealious: string;
};
export type FieldHandler<
Defaults extends Partial<FieldHandlerResult>,
Result extends Partial<FieldHandlerResult>
> = (
collection_name: string,
field: ExtractedFieldInfo,
importPath: (path: string) => string,
defaults: Defaults,
+ all_other_handlers: Record<
+ string,
+ | FieldHandler<FieldHandlerResult, Partial<FieldHandlerResult>>
+ | undefined
+ >,
fields_var?: string
) => Promise<Result>;
export const field_handlers: Record<
Exclude<string, "default">,
FieldHandler<FieldHandlerResult, Partial<FieldHandlerResult>> | undefined
> & {
default: FieldHandler<Record<string, never>, FieldHandlerResult>;
} = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
default: default_handler as any,
enum: enum_handler,
image: image_handler,
file: file_handler,
int: int_handler,
boolean: boolean_handler,
jdd: jdd_handler,
date: date_handler,
"deep-reverse-single-reference": deep_reverse_single_reference_handler,
"structured-array": structured_array_handler,
"single-reference": single_reference_handler,
};
export async function getFieldHandler(
collection_name: string,
field: ExtractedFieldInfo,
importPath: (path: string) => string,
fields_var?: string
) {
const defaults = await field_handlers.default(
collection_name,
field,
importPath,
{},
+ field_handlers,
fields_var
);
+
const result = {
...defaults,
...((await field_handlers[field.type]?.(
collection_name,
field,
importPath,
defaults,
+ field_handlers,
fields_var
)) || {}),
};
const result_initial_value_edit = result.initial_value_edit;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
result.initial_value_edit =
typeof result_initial_value_edit == "string"
? result_initial_value_edit
: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(defaults.initial_value_edit as any),
...result_initial_value_edit,
};
return result;
}
export async function sharedCrudFormFields(
collection_name: string,
newfilefullpath: string
) {
const [uppercase_collection, collection_fields] = await Promise.all([
extractCollectionClassname(
target_locreq.resolve(
"src/back/collections/" + collection_name + ".ts"
)
),
extract_fields_from_collection(collection_name),
]);
const importPath = curryImportPath(newfilefullpath);
const field_handler_results = await Promise.all(
collection_fields.map(async (field) => {
return getFieldHandler(collection_name, field, importPath);
})
);
const result = `import { Controls, Fields } from "@sealcode/sealgen";
import { ${uppercase_collection} } from "${importPath(
"src/back/collections/collections.js"
)}";
${Array.from(
new Set(
field_handler_results
.filter(
({ hide_field, hide_control }) => !hide_field && !hide_control
)
.map(({ imports }) => imports.shared || "")
)
).join("\n")}
export const ${toPascalCase(collection_name)}FormFields = <const>
{
${field_handler_results
.filter(({ hide_field }) => !hide_field)
.map(({ field }) => field)
.filter((e) => e != "")
.join(",\n")}
};
export const ${toPascalCase(collection_name)}FormControls = [
${field_handler_results
.filter(({ hide_control }) => !hide_control)
.map(({ controls }) => controls)
.filter((e) => e != "")
.join(",\n")}
];
${Array.from(
new Set(
field_handler_results.map(({ top_level_shared }) => top_level_shared)
)
).join("\n")}
`;
return formatWithPrettier(result);
}
diff --git a/src/utils/extract-fields-from-collection.ts b/src/utils/extract-fields-from-collection.ts
index 8a52900..850dc0a 100644
--- a/src/utils/extract-fields-from-collection.ts
+++ b/src/utils/extract-fields-from-collection.ts
@@ -1,86 +1,86 @@
import _locreq from "locreq";
import { exec } from "./utils.js";
const target_locreq = _locreq(process.cwd());
import { promises as fs } from "fs";
import { is, predicates } from "@sealcode/ts-predicates";
export type ExtractedFieldInfo = {
name: string;
type: string;
target: string | null;
referencing_collection: string | null;
referencing_field: string | null;
intermediary_field_that_points_there: string | null;
is_readonly: boolean;
is_required: boolean;
subfields: { name: string }[];
};
const counter = (function* () {
let i = 0;
while (true) {
yield i++;
}
})();
export default async function extract_fields_from_collection(
collection_name: string
): Promise<ExtractedFieldInfo[]> {
const extractor_code = `import {default as the_app} from "./app.js";
const c = new the_app().collections["${collection_name}"];
const fields = [];
for (const field_name in c.fields){
- const field = c.fields[field_name];
+ let field = c.fields[field_name];
let type = field.typeName;
let target = null;
let referencing_collection = null;
let referencing_field = null;
let intermediary_field_that_points_there = null;
let is_readonly = false;
let is_required = field.required;
let subfields = [];
- if(["derived-value", "cached-value"].includes(type)){
+ if(["derived-value", "cached-value", "settable-by"].includes(type)){
type = field.virtual_field.typeName;
}
if(["derived-value", "cached-value", "reverse-single-reference"].includes(field.typeName)){
is_readonly = true;
}
if(type == "deep-reverse-single-reference" || type =="reverse-single-reference"){
target = field.target_collection;
referencing_collection = field.referencing_collection;
referencing_field = field.referencing_field;
intermediary_field_that_points_there = field.intermediary_field_that_points_there
}
if( type == "structured-array" ) {
- subfields = Object.keys(field.subfields).map(key=>({name: key}));
+ subfields = Object.keys(field.subfields || field.virtual_field.subfields).map(key=>({name: key}));
}
fields.push({ name: field_name, type, target, referencing_collection, referencing_field, intermediary_field_that_points_there, is_readonly, is_required, subfields })
}
console.log(JSON.stringify(fields));
`;
const extractor_code_path = target_locreq.resolve(
`dist/back/___extract_fields--${counter.next().value || 0}.js`
);
await fs.writeFile(extractor_code_path, extractor_code);
const { stdout } = await exec("node", [extractor_code_path]);
await fs.unlink(extractor_code_path);
const ret = JSON.parse(stdout) as unknown;
if (
!is(
ret,
predicates.array(
predicates.shape({
name: predicates.string,
type: predicates.string,
is_readonly: predicates.boolean,
})
)
)
) {
throw new Error(
"Encountered a problem while extracting the names of fields from collection. Got: " +
stdout
);
}
return ret as ExtractedFieldInfo[];
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 08:40 (19 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034375
Default Alt Text
(24 KB)

Event Timeline