This commit is contained in:
dswbx
2025-06-21 17:05:27 +02:00
parent 42edce904f
commit 6e78a4c238
37 changed files with 215 additions and 125 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core";
import { $console } from "core/console";
import { Event } from "core/events";
import type { em as prototypeEm } from "data/prototype";
import { Connection } from "data/connection/Connection";
@@ -34,7 +34,7 @@ export class PasswordStrategy extends Strategy<typeof schema> {
private getPayloadSchema() {
return s.object({
email: s.string({
pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$",
pattern: /^[\w-\.\+_]+@([\w-]+\.)+[\w-]{2,4}$/,
}),
password: s.string({
minLength: 8, // @todo: this should be configurable
+7 -8
View File
@@ -26,14 +26,13 @@ export class SchemaObject<Schema extends TSchema = TSchema> {
initial?: Partial<s.Static<Schema>>,
private options?: SchemaObjectOptions<Schema>,
) {
this._default = _schema.template() as any;
this._value = initial
? parse(_schema, structuredClone(initial as any), {
withDefaults: true,
forceParse: this.isForceParse(),
skipMark: this.isForceParse(),
})
: (this._default as any);
this._default = _schema.template({}, { withOptional: true }) as any;
this._value = parse(_schema, structuredClone(initial ?? {}), {
withDefaults: true,
withExtendedDefaults: true,
forceParse: this.isForceParse(),
skipMark: this.isForceParse(),
});
this._config = Object.freeze(this._value);
}
+6 -2
View File
@@ -38,6 +38,7 @@ export class InvalidSchemaError extends Error {
export type ParseOptions = {
withDefaults?: boolean;
withExtendedDefaults?: boolean;
coerce?: boolean;
clone?: boolean;
skipMark?: boolean; // @todo: do something with this
@@ -57,8 +58,11 @@ export function parse<S extends s.Schema, Options extends ParseOptions = ParseOp
): Options extends { coerce: true } ? s.StaticCoerced<S> : s.Static<S> {
const schema = (opts?.clone ? cloneSchema(_schema as any) : _schema) as s.Schema;
let value = opts?.coerce !== false ? schema.coerce(v) : v;
if (opts?.withDefaults) {
value = schema.template(value, { withOptional: true });
if (opts?.withDefaults !== false) {
value = schema.template(value, {
withOptional: true,
withExtendedOptional: opts?.withExtendedDefaults ?? false,
});
}
const result = schema.validate(value, {
+1 -1
View File
@@ -120,7 +120,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
if (options.where) {
// @todo: auto-alias base entity when using joins! otherwise "id" is ambiguous
const aliases = [entity.name];
if (validated.join.length > 0) {
if (validated.join?.length > 0) {
aliases.push(...JoinBuilder.getJoinedEntityNames(this.em, entity, validated.join));
}
+1 -1
View File
@@ -47,7 +47,7 @@ export class InvalidFieldConfigException extends Exception {
) {
console.error("InvalidFieldConfigException", {
given,
error: error.firstToString(),
error: error.first(),
});
super(`Invalid Field config given for field "${field.name}": ${error.firstToString()}`);
}
+2 -1
View File
@@ -6,7 +6,8 @@ import { s } from "core/object/schema";
export const booleanFieldConfigSchema = s
.strictObject({
default_value: s.boolean({ default: false }),
//default_value: s.boolean({ default: false }),
default_value: s.boolean(),
...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
})
.partial();
+2 -2
View File
@@ -7,11 +7,11 @@ import { s } from "core/object/schema";
export const dateFieldConfigSchema = s
.strictObject({
type: s.string({ enum: ["date", "datetime", "week"], default: "date" }),
type: s.string({ enum: ["date", "datetime", "week"] }),
timezone: s.string(),
min_date: s.string(),
max_date: s.string(),
...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
...baseFieldConfigSchema.properties,
})
.partial();
+15 -23
View File
@@ -27,26 +27,16 @@ export const baseFieldConfigSchema = s
.strictObject({
label: s.string(),
description: s.string(),
required: s.boolean({ default: DEFAULT_REQUIRED }),
fillable: s.anyOf(
[
s.boolean({ title: "Boolean", default: DEFAULT_FILLABLE }),
s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }),
],
{
default: DEFAULT_FILLABLE,
},
),
hidden: s.anyOf(
[
s.boolean({ title: "Boolean", default: DEFAULT_HIDDEN }),
// @todo: tmp workaround
s.array(s.string({ enum: TmpContext }), { title: "Context", uniqueItems: true }),
],
{
default: DEFAULT_HIDDEN,
},
),
required: s.boolean(),
fillable: s.anyOf([
s.boolean({ title: "Boolean" }),
s.array(s.string({ enum: ActionContext }), { title: "Context", uniqueItems: true }),
]),
hidden: s.anyOf([
s.boolean({ title: "Boolean" }),
// @todo: tmp workaround
s.array(s.string({ enum: TmpContext }), { title: "Context", uniqueItems: true }),
]),
// if field is virtual, it will not call transformPersist & transformRetrieve
virtual: s.boolean(),
default_value: s.any(),
@@ -100,7 +90,9 @@ export abstract class Field<
name: this.name,
type: "text",
nullable: true,
dflt: this.getDefault(),
// see field-test-suite.ts:41
dflt: undefined,
//dflt: this.getDefault(),
});
}
@@ -116,14 +108,14 @@ export abstract class Field<
if (Array.isArray(this.config.fillable)) {
return context ? this.config.fillable.includes(context) : DEFAULT_FILLABLE;
}
return !!this.config.fillable;
return this.config.fillable ?? DEFAULT_FILLABLE;
}
isHidden(context?: TmpActionAndRenderContext): boolean {
if (Array.isArray(this.config.hidden)) {
return context ? this.config.hidden.includes(context as any) : DEFAULT_HIDDEN;
}
return this.config.hidden ?? false;
return this.config.hidden ?? DEFAULT_HIDDEN;
}
isRequired(): boolean {
+5 -5
View File
@@ -1,5 +1,5 @@
import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema";
import { FromSchema, objectToJsLiteral, omitKeys } from "core/utils";
import { objectToJsLiteral } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
@@ -8,10 +8,10 @@ import { s } from "core/object/schema";
export const jsonSchemaFieldConfigSchema = s
.strictObject({
schema: s.any({ type: "object", default: {} }),
ui_schema: s.any({ type: "object", default: {} }),
schema: s.any({ type: "object" }),
ui_schema: s.any({ type: "object" }),
default_from_schema: s.boolean(),
...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
...baseFieldConfigSchema.properties,
})
.partial();
@@ -26,7 +26,7 @@ export class JsonSchemaField<
constructor(name: string, config: Partial<JsonSchemaFieldConfig>) {
super(name, config);
this.validator = new Validator(this.getJsonSchema());
this.validator = new Validator({ ...this.getJsonSchema() });
}
protected getSchema() {
+1 -2
View File
@@ -3,7 +3,6 @@ import { omitKeys, uuidv7 } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field";
import type { TFieldTSType } from "data/entities/EntityTypescript";
import { s } from "core/object/schema";
import type { FieldSpec } from "data/connection/Connection";
export const primaryFieldTypes = ["integer", "uuid"] as const;
export type TPrimaryFieldFormat = (typeof primaryFieldTypes)[number];
@@ -26,7 +25,7 @@ export class PrimaryField<Required extends true | false = false> extends Field<
override readonly type = "primary";
constructor(name: string = config.data.default_primary_field, cfg?: PrimaryFieldConfig) {
super(name, { fillable: false, required: false, ...cfg });
super(name, { ...cfg, fillable: false, required: false });
}
override isRequired(): boolean {
+2 -2
View File
@@ -10,8 +10,8 @@ export const textFieldConfigSchema = s
minLength: s.number(),
maxLength: s.number(),
pattern: s.string(),
html_config: s.object({
element: s.string({ default: "input" }),
html_config: s.partialObject({
element: s.string(),
props: s.record(s.anyOf([s.string({ title: "String" }), s.number({ title: "Number" })])),
}),
...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
+5 -5
View File
@@ -50,7 +50,7 @@ export function fieldTestSuite(
expect(noConfigField.hasDefault()).toBe(false);
expect(noConfigField.getDefault()).toBeUndefined();
expect(dflt.hasDefault()).toBe(true);
expect(dflt.getDefault()).toBe(config.defaultValue);
expect(dflt.getDefault()).toEqual(config.defaultValue);
});
test("isFillable", async () => {
@@ -98,9 +98,6 @@ export function fieldTestSuite(
test("toJSON", async () => {
const _config = {
..._requiredConfig,
fillable: true,
required: false,
hidden: false,
};
function fieldJson(field: Field) {
@@ -118,7 +115,10 @@ export function fieldTestSuite(
expect(fieldJson(fillable)).toEqual({
type: noConfigField.type,
config: _config,
config: {
..._config,
fillable: true,
},
});
expect(fieldJson(required)).toEqual({
+6 -3
View File
@@ -141,9 +141,12 @@ export const repoQuery = s.recursive((self) =>
.partial(),
);
export const getRepoQueryTemplate = () =>
repoQuery.template({
withOptional: true,
}) as Required<RepoQuery>;
repoQuery.template(
{},
{
withOptional: true,
},
) as Required<RepoQuery>;
export type RepoQueryIn = {
limit?: number;
+1 -2
View File
@@ -17,8 +17,7 @@ export type TaskResult<Output = any> = {
export type TaskRenderProps<T extends Task = Task> = any;
// @todo: CURRENT WORKAROUND
export const dynamic = <S extends s.Schema>(a: S, b?: any) => null as unknown as S;
export const dynamic = <S extends s.Schema>(a: S, b?: any) => a;
/* export function dynamic<Type extends TSchema>(
type: Type,
+2 -1
View File
@@ -23,7 +23,8 @@ declare module "core" {
}
}
export class AppMedia extends Module<TAppMediaConfig> {
// @todo: current workaround to make it all required
export class AppMedia extends Module<Required<TAppMediaConfig>> {
private _storage?: Storage;
override async build() {
+37 -12
View File
@@ -23,21 +23,46 @@ export function buildMediaSchema() {
);
});
return s.strictObject({
enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }),
entity_name: s.string({ default: "media" }),
storage: s.strictObject(
return s
.strictObject(
{
body_max_size: s.number({
description: "Max size of the body in bytes. Leave blank for unlimited.",
}),
enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }),
entity_name: s.string({ default: "media" }),
storage: s
.strictObject({
body_max_size: s.number({
description: "Max size of the body in bytes. Leave blank for unlimited.",
}),
})
.partial(),
adapter: s.anyOf(Object.values(adapterSchemaObject)),
},
{ default: {} },
),
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(),
});
{
default: {},
},
)
.partial();
}
export const mediaConfigSchema = buildMediaSchema();
export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>;
export type TAppMediaConfig2 = s.ObjectDefaults<(typeof mediaConfigSchema)["properties"]>;
const schema = s.strictObject(
{
enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }),
entity_name: s.string({ default: "media" }),
storage: s
.strictObject({
body_max_size: s.number({
description: "Max size of the body in bytes. Leave blank for unlimited.",
}),
})
.partial(),
},
{
default: {},
},
);
+2 -2
View File
@@ -25,10 +25,10 @@ export type { ModuleBuildContext };
export const MODULES = {
server: AppServer,
data: AppData, // @todo:
data: AppData,
auth: AppAuth,
media: AppMedia,
flows: AppFlows, // @todo:
flows: AppFlows,
} as const;
// get names of MODULES as an array
+5 -5
View File
@@ -1,6 +1,6 @@
import type { JSONSchema7 } from "json-schema";
import { cloneDeep, omit, pick } from "lodash-es";
import type { s } from "core/object/schema";
import { omitKeys } from "core/utils";
export function extractSchema<
Schema extends s.ObjectSchema,
@@ -25,10 +25,10 @@ export function extractSchema<
return [{ ...schema.toJSON() }, config, {} as any];
}
const newSchema = cloneDeep(schema);
const newSchema = JSON.parse(JSON.stringify(schema));
const updated = {
...newSchema.toJSON(),
properties: omit(newSchema.properties, keys),
...newSchema,
properties: omitKeys(newSchema.properties, keys),
};
if (updated.required) {
updated.required = updated.required.filter((key) => !keys.includes(key as any));
@@ -44,7 +44,7 @@ export function extractSchema<
};
}
const reducedConfig = omit(config, keys) as any;
const reducedConfig = omitKeys(config, keys as string[]) as any;
return [updated, reducedConfig, extracted];
}