Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360786
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
24 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment