merged origin/release/0.12

This commit is contained in:
dswbx
2025-04-11 13:10:25 +02:00
79 changed files with 177 additions and 544 deletions
+1 -12
View File
@@ -1,4 +1,4 @@
import { isDebug, tbValidator as tb } from "core";
import { $console, isDebug, tbValidator as tb } from "core";
import { StringEnum } from "core/utils";
import * as tbbox from "@sinclair/typebox";
import {
@@ -47,7 +47,6 @@ export class DataController extends Controller {
const template = { data: res.data, meta };
// @todo: this works but it breaks in FE (need to improve DataTable)
//return objectCleanEmpty(template) as any;
// filter empty
return Object.fromEntries(
Object.entries(template).filter(([_, v]) => typeof v !== "undefined" && v !== null),
@@ -58,7 +57,6 @@ export class DataController extends Controller {
const template = { data: res.data };
// filter empty
//return objectCleanEmpty(template);
return Object.fromEntries(Object.entries(template).filter(([_, v]) => v !== undefined));
}
@@ -74,11 +72,6 @@ export class DataController extends Controller {
const { permission, auth } = this.middlewares;
const hono = this.create().use(auth(), permission(SystemPermissions.accessApi));
const definedEntities = this.em.entities.map((e) => e.name);
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
.Decode(Number.parseInt)
.Encode(String);
// @todo: sample implementation how to augment handler with additional info
function handler<HH extends Handler>(name: string, h: HH): any {
const func = h;
@@ -143,10 +136,8 @@ export class DataController extends Controller {
}),
),
async (c) => {
//console.log("request", c.req.raw);
const { entity, context } = c.req.param();
if (!this.entityExists(entity)) {
console.warn("not found:", entity, definedEntities);
return this.notFound(c);
}
const _entity = this.em.entity(entity);
@@ -256,7 +247,6 @@ export class DataController extends Controller {
async (c) => {
const { entity } = c.req.param();
if (!this.entityExists(entity)) {
console.warn("not found:", entity, definedEntities);
return this.notFound(c);
}
const options = c.req.valid("query") as RepoQuery;
@@ -330,7 +320,6 @@ export class DataController extends Controller {
return this.notFound(c);
}
const options = (await c.req.valid("json")) as RepoQuery;
//console.log("options", options);
const result = await this.em.repository(entity).findMany(options);
return c.json(this.repoResult(result), { status: result.data ? 200 : 404 });
@@ -38,7 +38,7 @@ export class LibsqlConnection extends SqliteConnection {
if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
console.log("changing protocol to", protocol);
$console.log("changing protocol to", protocol);
const [, rest] = url.split("://");
url = `${protocol}://${rest}`;
}
-2
View File
@@ -35,7 +35,6 @@ export type TAppDataField = Static<typeof fieldsSchema>;
export type TAppDataEntityFields = Static<typeof entityFields>;
export const entitiesSchema = tb.Type.Object({
//name: Type.String(),
type: tb.Type.Optional(
tb.Type.String({ enum: entityTypes, default: "regular", readOnly: true }),
),
@@ -63,7 +62,6 @@ export const indicesSchema = tb.Type.Object(
{
entity: tb.Type.String(),
fields: tb.Type.Array(tb.Type.String(), { minItems: 1 }),
//name: Type.Optional(Type.String()),
unique: tb.Type.Optional(tb.Type.Boolean({ default: false })),
},
{
+10 -7
View File
@@ -1,4 +1,4 @@
import { config } from "core";
import { $console, config } from "core";
import {
type Static,
StringEnum,
@@ -184,9 +184,9 @@ export class Entity<
if (existing) {
// @todo: for now adding a graceful method
if (JSON.stringify(existing) === JSON.stringify(field)) {
/*console.warn(
$console.warn(
`Field "${field.name}" already exists on entity "${this.name}", but it's the same, so skipping.`,
);*/
);
return;
}
@@ -233,7 +233,13 @@ export class Entity<
for (const field of fields) {
if (!field.isValid(data[field.name], context)) {
console.log("Entity.isValidData:invalid", context, field.name, data[field.name]);
$console.warn(
"invalid data given for",
this.name,
context,
field.name,
data[field.name],
);
if (options?.explain) {
throw new Error(`Field "${field.name}" has invalid data: "${data[field.name]}"`);
}
@@ -259,7 +265,6 @@ export class Entity<
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
const schema = Type.Object(
transformObject(_fields, (field) => {
//const hidden = field.isHidden(options?.context);
const fillable = field.isFillable(options?.context);
return {
title: field.config.label,
@@ -277,9 +282,7 @@ export class Entity<
toJSON() {
return {
//name: this.name,
type: this.type,
//fields: transformObject(this.fields, (field) => field.toJSON()),
fields: Object.fromEntries(this.fields.map((field) => [field.name, field.toJSON()])),
config: this.config,
};
+4 -5
View File
@@ -1,4 +1,4 @@
import type { DB as DefaultDB } from "core";
import { $console, type DB as DefaultDB } from "core";
import { EventManager } from "core/events";
import { sql } from "kysely";
import { Connection } from "../connection/Connection";
@@ -55,7 +55,6 @@ export class EntityManager<TBD extends object = DefaultDB> {
this.connection = connection;
this.emgr = emgr ?? new EventManager();
//console.log("registering events", EntityManager.Events);
this.emgr.registerEvents(EntityManager.Events);
}
@@ -90,7 +89,9 @@ export class EntityManager<TBD extends object = DefaultDB> {
if (existing) {
// @todo: for now adding a graceful method
if (JSON.stringify(existing) === JSON.stringify(entity)) {
//console.warn(`Entity "${entity.name}" already exists, but it's the same, so skipping.`);
$console.warn(
`Entity "${entity.name}" already exists, but it's the same, skipping adding it.`,
);
return;
}
@@ -108,7 +109,6 @@ export class EntityManager<TBD extends object = DefaultDB> {
}
this._entities[entityIndex] = entity;
// caused issues because this.entity() was using a reference (for when initial config was given)
}
@@ -295,7 +295,6 @@ export class EntityManager<TBD extends object = DefaultDB> {
return {
entities: Object.fromEntries(this.entities.map((e) => [e.name, e.toJSON()])),
relations: Object.fromEntries(this.relations.all.map((r) => [r.getName(), r.toJSON()])),
//relations: this.relations.all.map((r) => r.toJSON()),
indices: Object.fromEntries(this.indices.map((i) => [i.name, i.toJSON()])),
};
}
+2 -3
View File
@@ -1,4 +1,4 @@
import type { DB as DefaultDB, PrimaryFieldType } from "core";
import { $console, type DB as DefaultDB, type PrimaryFieldType } from "core";
import { type EmitsEvents, EventManager } from "core/events";
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
import { type TActionContext, WhereBuilder } from "..";
@@ -72,7 +72,6 @@ export class Mutator<
// if relation field (include key and value in validatedData)
if (Array.isArray(result)) {
//console.log("--- (instructions)", result);
const [relation_key, relation_value] = result;
validatedData[relation_key] = relation_value;
}
@@ -122,7 +121,7 @@ export class Mutator<
};
} catch (e) {
// @todo: redact
console.log("[Error in query]", sql);
$console.error("[Error in query]", sql);
throw e;
}
}
-1
View File
@@ -49,7 +49,6 @@ export class BooleanField<Required extends true | false = false> extends Field<
}
override transformRetrieve(value: unknown): boolean | null {
//console.log("Boolean:transformRetrieve:value", value);
if (typeof value === "undefined" || value === null) {
if (this.isRequired()) return false;
if (this.hasDefault()) return this.getDefault();
+3 -11
View File
@@ -1,13 +1,13 @@
import { type Static, StringEnum, dayjs } from "core/utils";
import type { EntityManager } from "../entities";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import { $console } from "core";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export const dateFieldConfigSchema = Type.Composite(
[
Type.Object({
//default_value: Type.Optional(Type.Date()),
type: StringEnum(["date", "datetime", "week"] as const, { default: "date" }),
timezone: Type.Optional(Type.String()),
min_date: Type.Optional(Type.String()),
@@ -53,13 +53,11 @@ export class DateField<Required extends true | false = false> extends Field<
}
private parseDateFromString(value: string): Date {
//console.log("parseDateFromString", value);
if (this.config.type === "week" && value.includes("-W")) {
const [year, week] = value.split("-W").map((n) => Number.parseInt(n, 10)) as [
number,
number,
];
//console.log({ year, week });
// @ts-ignore causes errors on build?
return dayjs().year(year).week(week).toDate();
}
@@ -69,15 +67,12 @@ export class DateField<Required extends true | false = false> extends Field<
override getValue(value: string, context?: TRenderContext): string | undefined {
if (value === null || !value) return;
//console.log("getValue", { value, context });
const date = this.parseDateFromString(value);
//console.log("getValue.date", date);
if (context === "submit") {
try {
return date.toISOString();
} catch (e) {
//console.warn("DateField.getValue:value/submit", value, e);
return undefined;
}
}
@@ -86,7 +81,7 @@ export class DateField<Required extends true | false = false> extends Field<
try {
return `${date.getFullYear()}-W${dayjs(date).week()}`;
} catch (e) {
console.warn("error - DateField.getValue:week", value, e);
$console.warn("DateField.getValue:week error", value, String(e));
return;
}
}
@@ -99,8 +94,7 @@ export class DateField<Required extends true | false = false> extends Field<
return this.formatDate(local);
} catch (e) {
console.warn("DateField.getValue:value", value);
console.warn("DateField.getValue:e", e);
$console.warn("DateField.getValue error", this.config.type, value, String(e));
return;
}
}
@@ -119,7 +113,6 @@ export class DateField<Required extends true | false = false> extends Field<
}
override transformRetrieve(_value: string): Date | null {
//console.log("transformRetrieve DateField", _value);
const value = super.transformRetrieve(_value);
if (value === null) return null;
@@ -138,7 +131,6 @@ export class DateField<Required extends true | false = false> extends Field<
const value = await super.transformPersist(_value, em, context);
if (this.nullish(value)) return value;
//console.log("transformPersist DateField", value);
switch (this.config.type) {
case "date":
case "week":
+1 -9
View File
@@ -1,7 +1,7 @@
import { Const, type Static, StringEnum } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
@@ -55,10 +55,6 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
constructor(name: string, config: Partial<EnumFieldConfig>) {
super(name, config);
/*if (this.config.options.values.length === 0) {
throw new Error(`Enum field "${this.name}" requires at least one option`);
}*/
if (this.config.default_value && !this.isValidValue(this.config.default_value)) {
throw new Error(`Default value "${this.config.default_value}" is not a valid option`);
}
@@ -71,10 +67,6 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
getOptions(): { label: string; value: string }[] {
const options = this.config?.options ?? { type: "strings", values: [] };
/*if (options.values?.length === 0) {
throw new Error(`Enum field "${this.name}" requires at least one option`);
}*/
if (options.type === "strings") {
return options.values?.map((option) => ({ label: option, value: option }));
}
-1
View File
@@ -84,7 +84,6 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
context: TActionContext,
): Promise<string | undefined> {
const value = await super.transformPersist(_value, em, context);
//console.log("value", value);
if (this.nullish(value)) return value;
if (!this.isSerializable(value)) {
-11
View File
@@ -48,22 +48,16 @@ export class JsonSchemaField<
override isValid(value: any, context: TActionContext = "update"): boolean {
const parentValid = super.isValid(value, context);
//console.log("jsonSchemaField:isValid", this.getJsonSchema(), this.name, value, parentValid);
if (parentValid) {
// already checked in parent
if (!this.isRequired() && (!value || typeof value !== "object")) {
//console.log("jsonschema:valid: not checking", this.name, value, context);
return true;
}
const result = this.validator.validate(value);
//console.log("jsonschema:errors", this.name, result.errors);
return result.valid;
} else {
//console.log("jsonschema:invalid", this.name, value, context);
}
//console.log("jsonschema:invalid:fromParent", this.name, value, context);
return false;
}
@@ -91,7 +85,6 @@ export class JsonSchemaField<
try {
return Default(FromSchema(this.getJsonSchema()), {});
} catch (e) {
//console.error("jsonschema:transformRetrieve", e);
return null;
}
} else if (this.hasDefault()) {
@@ -109,13 +102,9 @@ export class JsonSchemaField<
): Promise<string | undefined> {
const value = await super.transformPersist(_value, em, context);
if (this.nullish(value)) return value;
//console.log("jsonschema:transformPersist", this.name, _value, context);
if (!this.isValid(value)) {
//console.error("jsonschema:transformPersist:invalid", this.name, value);
throw new TransformPersistFailedException(this.name, value);
} else {
//console.log("jsonschema:transformPersist:valid", this.name, value);
}
if (!value || typeof value !== "object") return this.getDefault();
-9
View File
@@ -98,12 +98,9 @@ export function fieldTestSuite(
test("toJSON", async () => {
const _config = {
..._requiredConfig,
//order: 1,
fillable: true,
required: false,
hidden: false,
//virtual: false,
//default_value: undefined
};
function fieldJson(field: Field) {
@@ -115,19 +112,16 @@ export function fieldTestSuite(
}
expect(fieldJson(noConfigField)).toEqual({
//name: "no_config",
type: noConfigField.type,
config: _config,
});
expect(fieldJson(fillable)).toEqual({
//name: "fillable",
type: noConfigField.type,
config: _config,
});
expect(fieldJson(required)).toEqual({
//name: "required",
type: required.type,
config: {
..._config,
@@ -136,7 +130,6 @@ export function fieldTestSuite(
});
expect(fieldJson(hidden)).toEqual({
//name: "hidden",
type: required.type,
config: {
..._config,
@@ -145,7 +138,6 @@ export function fieldTestSuite(
});
expect(fieldJson(dflt)).toEqual({
//name: "dflt",
type: dflt.type,
config: {
..._config,
@@ -154,7 +146,6 @@ export function fieldTestSuite(
});
expect(fieldJson(requiredAndDefault)).toEqual({
//name: "full",
type: requiredAndDefault.type,
config: {
..._config,
@@ -39,7 +39,6 @@ export class EntityIndex {
return {
entity: this.entity.name,
fields: this.fields.map((f) => f.name),
//name: this.name,
unique: this.unique,
};
}
-12
View File
@@ -18,7 +18,6 @@ export function getChangeSet(
data: EntityData,
fields: Field[],
): EntityData {
//console.log("getChangeSet", formData, data);
return transform(
formData,
(acc, _value, key) => {
@@ -32,17 +31,6 @@ export function getChangeSet(
// @todo: add typing for "action"
if (action === "create" || newValue !== data[key]) {
acc[key] = newValue;
/*console.log("changed", {
key,
value,
valueType: typeof value,
prev: data[key],
newValue,
new: value,
sent: acc[key]
});*/
} else {
//console.log("no change", key, value, data[key]);
}
},
{} as typeof formData,
-10
View File
@@ -14,15 +14,6 @@ const { Type } = tbbox;
export type KyselyJsonFrom = any;
export type KyselyQueryBuilder = SelectQueryBuilder<any, any, any>;
/*export type RelationConfig = {
mappedBy?: string;
inversedBy?: string;
sourceCardinality?: number;
connectionTable?: string;
connectionTableMappedName?: string;
required?: boolean;
};*/
export type BaseRelationConfig = Static<typeof EntityRelation.schema>;
// @todo: add generic type for relation config
@@ -167,7 +158,6 @@ export abstract class EntityRelation<
* @param entity
*/
isListableFor(entity: Entity): boolean {
//console.log("isListableFor", entity.name, this.source.entity.name, this.target.entity.name);
return this.target.entity.name === entity.name;
}
+2 -3
View File
@@ -1,9 +1,9 @@
import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely";
import { Entity, type EntityManager } from "../entities";
import { type Field, PrimaryField, VirtualField } from "../fields";
import { type Field, PrimaryField } from "../fields";
import type { RepoQuery } from "../server/data-query-impl";
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { RelationField } from "./RelationField";
import { type RelationType, RelationTypes } from "./relation-types";
@@ -48,7 +48,6 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
this.connectionTableMappedName = config?.connectionTableMappedName || connectionTable;
this.additionalFields = additionalFields || [];
//this.connectionTable = connectionTable;
}
static defaultConnectionTable(source: Entity, target: Entity) {
+1 -2
View File
@@ -4,7 +4,7 @@ import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely";
import type { Entity, EntityManager } from "../entities";
import type { RepoQuery } from "../server/data-query-impl";
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { RelationField, type RelationFieldBaseConfig } from "./RelationField";
import type { MutationInstructionResponse } from "./RelationMutator";
@@ -127,7 +127,6 @@ export class ManyToOneRelation extends EntityRelation<typeof ManyToOneRelation.s
}
const groupBy = `${entity.name}.${entity.getPrimaryField().name}`;
//console.log("queryInfo", entity.name, { reference, side, relationRef, entityRef, otherRef });
return {
other,
+1 -16
View File
@@ -17,11 +17,6 @@ export const relationFieldConfigSchema = Type.Composite([
on_delete: Type.Optional(StringEnum(CASCADES, { default: "set null" })),
}),
]);
/*export const relationFieldConfigSchema = baseFieldConfigSchema.extend({
reference: z.string(),
target: z.string(),
target_field: z.string().catch("id"),
});*/
export type RelationFieldConfig = Static<typeof relationFieldConfigSchema>;
export type RelationFieldBaseConfig = { label?: string };
@@ -33,16 +28,6 @@ export class RelationField extends Field<RelationFieldConfig> {
return relationFieldConfigSchema;
}
/*constructor(name: string, config?: Partial<RelationFieldConfig>) {
//relation_name = relation_name || target.name;
//const name = [relation_name, target.getPrimaryField().name].join("_");
super(name, config);
//console.log(this.config);
//this.relation.target = target;
//this.relation.name = relation_name;
}*/
static create(
relation: EntityRelation,
target: EntityRelationAnchor,
@@ -52,7 +37,7 @@ export class RelationField extends Field<RelationFieldConfig> {
target.reference ?? target.entity.name,
target.entity.getPrimaryField().name,
].join("_");
//console.log('name', name);
return new RelationField(name, {
...config,
required: relation.required,
@@ -63,7 +63,6 @@ export class RelationMutator {
// make sure it's a primitive value
// @todo: this is not a good way of checking primitives. Null is also an object
if (typeof value === "object") {
console.log("value", value);
throw new Error(`Invalid value for relation field "${key}" given, expected primitive.`);
}
-78
View File
@@ -1,78 +0,0 @@
type Field<Type, Required extends true | false> = {
_type: Type;
_required: Required;
};
type TextField<Required extends true | false = false> = Field<string, Required> & {
_type: string;
required: () => TextField<true>;
};
type NumberField<Required extends true | false = false> = Field<number, Required> & {
_type: number;
required: () => NumberField<true>;
};
type Entity<Fields extends Record<string, Field<any, any>> = {}> = { name: string; fields: Fields };
function entity<Fields extends Record<string, Field<any, any>>>(
name: string,
fields: Fields,
): Entity<Fields> {
return { name, fields };
}
function text(): TextField<false> {
return {} as any;
}
function number(): NumberField<false> {
return {} as any;
}
const field1 = text();
const field1_req = text().required();
const field2 = number();
const user = entity("users", {
name: text().required(),
bio: text(),
age: number(),
some: number().required(),
});
type InferEntityFields<T> = T extends Entity<infer Fields>
? {
[K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required }
? Required extends true
? Type
: Type | undefined
: never;
}
: never;
type Prettify<T> = {
[K in keyof T]: T[K];
};
export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
// from https://github.com/type-challenges/type-challenges/issues/28200
type Merge<T> = {
[K in keyof T]: T[K];
};
type OptionalUndefined<
T,
Props extends keyof T = keyof T,
OptionsProps extends keyof T = Props extends keyof T
? undefined extends T[Props]
? Props
: never
: never,
> = Merge<
{
[K in OptionsProps]?: T[K];
} & {
[K in Exclude<keyof T, OptionsProps>]: T[K];
}
>;
type UserFields = InferEntityFields<typeof user>;
type UserFields2 = Simplify<OptionalUndefined<UserFields>>;
const obj: UserFields2 = { name: "h", age: 1, some: 1 };