Release 0.12 (#143)

* changed tb imports

* cleanup: replace console.log/warn with $console, remove commented-out code

Removed various commented-out code and replaced direct `console.log` and `console.warn` usage across the codebase with `$console` from "core" for standardized logging. Also adjusted linting rules in biome.json to enable warnings for `console.log` usage.

* ts: enable incremental

* fix imports in test files

reorganize imports to use "@sinclair/typebox" directly, replacing local utility references, and add missing "override" keywords in test classes.

* added media permissions (#142)

* added permissions support for media module

introduced `MediaPermissions` for fine-grained access control in the media module, updated routes to enforce these permissions, and adjusted permission registration logic.

* fix: handle token absence in getUploadHeaders and add tests for transport modes

ensure getUploadHeaders does not set Authorization header when token is missing. Add unit tests to validate behavior for different token_transport options.

* remove console.log on DropzoneContainer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add bcrypt and refactored auth resolve (#147)

* reworked auth architecture with improved password handling and claims

Refactored password strategy to prepare supporting bcrypt, improving hashing/encryption flexibility. Updated authentication flow with enhanced user resolution mechanisms, safe JWT generation, and consistent profile handling. Adjusted dependencies to include bcryptjs and updated lock files accordingly.

* fix strategy forms handling, add register route and hidden fields

Refactored strategy forms to include hidden fields for type and name. Added a registration route with necessary adjustments to the admin controller and routes. Corrected field handling within relevant forms and components.

* refactored auth handling to support bcrypt, extracted user pool

* update email regex to allow '+' and '_' characters

* update test stub password for AppAuth spec

* update data exceptions to use HttpStatus constants, adjust logging level in AppUserPool

* rework strategies to extend a base class instead of interface

* added simple bcrypt test

* add validation logs and improve data validation handling (#157)

Added warning logs for invalid data during mutator validation, refined field validation logic to handle undefined values, and adjusted event validation comments for clarity. Minor improvements include exporting events from core and handling optional chaining in entity field validation.

* modify MediaApi to support custom fetch implementation, defaults to native fetch (#158)

* modify MediaApi to support custom fetch implementation, defaults to native fetch

added an optional `fetcher` parameter to allow usage of a custom fetch function in both `upload` and `fetcher` methods. Defaults to the standard `fetch` if none is provided.

* fix tests and improve api fetcher types

* update admin basepath handling and window context integration (#155)

Refactored `useBkndWindowContext` to include `admin_basepath` and updated its usage in routing. Improved type consistency with `AdminBkndWindowContext` and ensured default values are applied for window context.

* trigger `repository-find-[one|many]-[before|after]` based on `limit` (#160)

* refactor error handling in authenticator and password strategy (#161)

made `respondWithError` method public, updated login and register routes in `PasswordStrategy` to handle errors using `respondWithError` for consistency.

* add disableSubmitOnError prop to NativeForm and export getFlashMessage (#162)

Introduced a `disableSubmitOnError` prop to NativeForm to control submit button behavior when errors are present. Also exported `getFlashMessage` from the core for external usage.

* update dependencies in package.json (#156)

moved several dependencies between devDependencies and dependencies for better categorization and removed redundant entries.

* update imports to adjust nodeTestRunner path and remove unused export (#163)

updated imports in test files to reflect the correct path for nodeTestRunner. removed redundant export of nodeTestRunner from index file to clean up module structure. In some environments this could cause issues requiring to exclude `node:test`, just removing it for now.

* fix sync events not awaited (#164)

* refactor(dropzone): extract DropzoneInner and unify state management with zustand (#165)

Simplified Dropzone implementation by extracting inner logic to a new component, `DropzoneInner`. Replaced local dropzone state logic with centralized state management using zustand. Adjusted API exports and props accordingly for consistency and maintainability.

* replace LiquidJs rendering with simplified renderer (#167)

* replace LiquidJs rendering with simplified renderer

Removed dependency on LiquidJS and replaced it with a custom templating solution using lodash `get`. Updated corresponding components, editors, and tests to align with the new rendering approach. Removed unused filters and tags.

* remove liquid js from package json

* feat/cli-generate-types (#166)

* init types generation

* update type generation for entities and fields

Refactored `EntityTypescript` to support improved field types and relations. Added `toType` method overrides for various fields to define accurate TypeScript types. Enhanced CLI `types` command with new options for output style and file handling. Removed redundant test files.

* update type generation code and CLI option description

removed unused imports definition, adjusted formatting in EntityTypescript, and clarified the CLI style option description.

* fix json schema field type generation

* reworked system entities to prevent recursive types

* reworked system entities to prevent recursive types

* remove unused object function

* types: use number instead of Generated

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
dswbx
2025-05-01 10:12:18 +02:00
committed by GitHub
parent d6f94a2ce1
commit 372f94d22a
186 changed files with 2617 additions and 1997 deletions

View File

@@ -1,5 +1,6 @@
import type { DB } from "core";
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
import type { Insertable, Selectable, Updateable } from "kysely";
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
@@ -25,21 +26,23 @@ export class DataApi extends ModuleApi<DataApiOptions> {
}
}
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
readOne<E extends keyof DB | string>(
entity: E,
id: PrimaryFieldType,
query: Omit<RepoQueryIn, "where" | "limit" | "offset"> = {},
) {
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>(
["entity", entity as any, id],
query,
);
}
readOneBy<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
readOneBy<E extends keyof DB | string>(
entity: E,
query: Omit<RepoQueryIn, "limit" | "offset" | "sort"> = {},
) {
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
type T = Pick<RepositoryResponse<Data>, "meta" | "data">;
return this.readMany(entity, {
...query,
@@ -48,10 +51,8 @@ export class DataApi extends ModuleApi<DataApiOptions> {
}).refine((data) => data[0]) as unknown as FetchPromise<ResponseObject<T>>;
}
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
query: RepoQueryIn = {},
) {
readMany<E extends keyof DB | string>(entity: E, query: RepoQueryIn = {}) {
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
type T = Pick<RepositoryResponse<Data[]>, "meta" | "data">;
const input = query ?? this.options.defaultQuery;
@@ -64,68 +65,70 @@ export class DataApi extends ModuleApi<DataApiOptions> {
return this.post<T>(["entity", entity as any, "query"], input);
}
readManyByReference<
E extends keyof DB | string,
R extends keyof DB | string,
Data = R extends keyof DB ? DB[R] : EntityData,
>(entity: E, id: PrimaryFieldType, reference: R, query: RepoQueryIn = {}) {
readManyByReference<E extends keyof DB | string, R extends keyof DB | string>(
entity: E,
id: PrimaryFieldType,
reference: R,
query: RepoQueryIn = {},
) {
type Data = R extends keyof DB ? Selectable<DB[R]> : EntityData;
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
["entity", entity as any, id, reference],
query ?? this.options.defaultQuery,
);
}
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
createOne<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
input: Omit<Data, "id">,
input: Insertable<Input>,
) {
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
}
createMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
createMany<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
input: Omit<Data, "id">[],
input: Insertable<Input>[],
) {
if (!input || !Array.isArray(input) || input.length === 0) {
throw new Error("input is required");
}
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.post<RepositoryResponse<Data[]>>(["entity", entity as any], input);
}
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
updateOne<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
id: PrimaryFieldType,
input: Partial<Omit<Data, "id">>,
input: Updateable<Input>,
) {
if (!id) throw new Error("ID is required");
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.patch<RepositoryResponse<Data>>(["entity", entity as any, id], input);
}
updateMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
updateMany<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
where: RepoQueryIn["where"],
update: Partial<Omit<Data, "id">>,
update: Updateable<Input>,
) {
this.requireObjectSet(where);
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.patch<RepositoryResponse<Data[]>>(["entity", entity as any], {
update,
where,
});
}
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
id: PrimaryFieldType,
) {
deleteOne<E extends keyof DB | string>(entity: E, id: PrimaryFieldType) {
if (!id) throw new Error("ID is required");
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.delete<RepositoryResponse<Data>>(["entity", entity as any, id]);
}
deleteMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
where: RepoQueryIn["where"],
) {
deleteMany<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"]) {
this.requireObjectSet(where);
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
return this.delete<RepositoryResponse<Data>>(["entity", entity as any], where);
}

View File

@@ -1,5 +1,6 @@
import { isDebug, tbValidator as tb } from "core";
import { StringEnum, Type } from "core/utils";
import { $console, isDebug, tbValidator as tb } from "core";
import { StringEnum } from "core/utils";
import * as tbbox from "@sinclair/typebox";
import {
DataPermissions,
type EntityData,
@@ -14,6 +15,7 @@ import type { ModuleBuildContext } from "modules";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions";
import type { AppDataConfig } from "../data-schema";
const { Type } = tbbox;
export class DataController extends Controller {
constructor(
@@ -45,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),
@@ -56,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));
}
@@ -72,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;
@@ -141,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);
@@ -254,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;
@@ -328,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 });

View File

@@ -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}`;
}

View File

@@ -1,4 +1,5 @@
import { type Static, StringRecord, Type, objectTransform } from "core/utils";
import { type Static, StringRecord, objectTransform } from "core/utils";
import * as tb from "@sinclair/typebox";
import {
FieldClassMap,
RelationClassMap,
@@ -18,36 +19,37 @@ export type FieldType = keyof typeof FIELDS;
export const RELATIONS = RelationClassMap;
export const fieldsSchemaObject = objectTransform(FIELDS, (field, name) => {
return Type.Object(
return tb.Type.Object(
{
type: Type.Const(name, { default: name, readOnly: true }),
config: Type.Optional(field.schema),
type: tb.Type.Const(name, { default: name, readOnly: true }),
config: tb.Type.Optional(field.schema),
},
{
title: name,
},
);
});
export const fieldsSchema = Type.Union(Object.values(fieldsSchemaObject));
export const fieldsSchema = tb.Type.Union(Object.values(fieldsSchemaObject));
export const entityFields = StringRecord(fieldsSchema);
export type TAppDataField = Static<typeof fieldsSchema>;
export type TAppDataEntityFields = Static<typeof entityFields>;
export const entitiesSchema = Type.Object({
//name: Type.String(),
type: Type.Optional(Type.String({ enum: entityTypes, default: "regular", readOnly: true })),
config: Type.Optional(entityConfigSchema),
fields: Type.Optional(entityFields),
export const entitiesSchema = tb.Type.Object({
type: tb.Type.Optional(
tb.Type.String({ enum: entityTypes, default: "regular", readOnly: true }),
),
config: tb.Type.Optional(entityConfigSchema),
fields: tb.Type.Optional(entityFields),
});
export type TAppDataEntity = Static<typeof entitiesSchema>;
export const relationsSchema = Object.entries(RelationClassMap).map(([name, relationClass]) => {
return Type.Object(
return tb.Type.Object(
{
type: Type.Const(name, { default: name, readOnly: true }),
source: Type.String(),
target: Type.String(),
config: Type.Optional(relationClass.schema),
type: tb.Type.Const(name, { default: name, readOnly: true }),
source: tb.Type.String(),
target: tb.Type.String(),
config: tb.Type.Optional(relationClass.schema),
},
{
title: name,
@@ -56,24 +58,23 @@ export const relationsSchema = Object.entries(RelationClassMap).map(([name, rela
});
export type TAppDataRelation = Static<(typeof relationsSchema)[number]>;
export const indicesSchema = Type.Object(
export const indicesSchema = tb.Type.Object(
{
entity: Type.String(),
fields: Type.Array(Type.String(), { minItems: 1 }),
//name: Type.Optional(Type.String()),
unique: Type.Optional(Type.Boolean({ default: false })),
entity: tb.Type.String(),
fields: tb.Type.Array(tb.Type.String(), { minItems: 1 }),
unique: tb.Type.Optional(tb.Type.Boolean({ default: false })),
},
{
additionalProperties: false,
},
);
export const dataConfigSchema = Type.Object(
export const dataConfigSchema = tb.Type.Object(
{
basepath: Type.Optional(Type.String({ default: "/api/data" })),
entities: Type.Optional(StringRecord(entitiesSchema, { default: {} })),
relations: Type.Optional(StringRecord(Type.Union(relationsSchema), { default: {} })),
indices: Type.Optional(StringRecord(indicesSchema, { default: {} })),
basepath: tb.Type.Optional(tb.Type.String({ default: "/api/data" })),
entities: tb.Type.Optional(StringRecord(entitiesSchema, { default: {} })),
relations: tb.Type.Optional(StringRecord(tb.Type.Union(relationsSchema), { default: {} })),
indices: tb.Type.Optional(StringRecord(indicesSchema, { default: {} })),
},
{
additionalProperties: false,

View File

@@ -1,13 +1,14 @@
import { config } from "core";
import { $console, config } from "core";
import {
type Static,
StringEnum,
Type,
parse,
snakeToPascalWithSpaces,
transformObject,
} from "core/utils";
import { type Field, PrimaryField, type TActionContext, type TRenderContext } from "../fields";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
// @todo: entity must be migrated to typebox
export const entityConfigSchema = Type.Object(
@@ -183,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;
}
@@ -231,8 +232,14 @@ 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]);
if (!field.isValid(data?.[field.name], context)) {
$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]}"`);
}
@@ -258,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,
@@ -274,11 +280,18 @@ export class Entity<
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
}
toTypes() {
return {
name: this.name,
type: this.type,
comment: this.config.description,
fields: Object.fromEntries(this.getFields().map((field) => [field.name, field.toType()])),
};
}
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,
};

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()])),
};
}

View File

@@ -0,0 +1,228 @@
import type { Entity, EntityManager, EntityRelation, TEntityType } from "data";
import { autoFormatString } from "core/utils";
import { AppAuth, AppMedia } from "modules";
export type TEntityTSType = {
name: string;
type: TEntityType;
comment?: string;
fields: Record<string, TFieldTSType>;
};
// [select, insert, update]
type TFieldContextType = boolean | [boolean, boolean, boolean];
export type TFieldTSType = {
required?: TFieldContextType;
fillable?: TFieldContextType;
type: "PrimaryFieldType" | string;
comment?: string;
import?: {
package: string;
name: string;
}[];
};
export type EntityTypescriptOptions = {
indentWidth?: number;
indentChar?: string;
entityCommentMultiline?: boolean;
fieldCommentMultiline?: boolean;
};
// keep a local copy here until properties have a type
const systemEntities = {
users: AppAuth.usersFields,
media: AppMedia.mediaFields,
};
export class EntityTypescript {
constructor(
protected em: EntityManager,
protected _options: EntityTypescriptOptions = {},
) {}
get options() {
return {
...this._options,
indentWidth: 2,
indentChar: " ",
entityCommentMultiline: true,
fieldCommentMultiline: false,
};
}
toTypes() {
return this.em.entities.map((e) => e.toTypes());
}
protected getTab(count = 1) {
return this.options.indentChar.repeat(this.options.indentWidth).repeat(count);
}
collectImports(
type: TEntityTSType,
imports: Record<string, string[]> = {},
): Record<string, string[]> {
for (const [, entity_type] of Object.entries(type.fields)) {
for (const imp of entity_type.import ?? []) {
const name = imp.name;
const pkg = imp.package;
if (!imports[pkg]) {
imports[pkg] = [];
}
if (!imports[pkg].includes(name)) {
imports[pkg].push(name);
}
}
}
return imports;
}
typeName(name: string) {
return autoFormatString(name);
}
fieldTypesToString(type: TEntityTSType, opts?: { ignore_fields?: string[]; indent?: number }) {
let string = "";
const coment_multiline = this.options.fieldCommentMultiline;
const indent = opts?.indent ?? 1;
for (const [field_name, field_type] of Object.entries(type.fields)) {
if (opts?.ignore_fields?.includes(field_name)) continue;
let f = "";
f += this.commentString(field_type.comment, indent, coment_multiline);
f += `${this.getTab(indent)}${field_name}${field_type.required ? "" : "?"}: `;
f += field_type.type + ";";
f += "\n";
string += f;
}
return string;
}
relationToFieldType(relation: EntityRelation, entity: Entity) {
const other = relation.other(entity);
const listable = relation.isListableFor(entity);
const name = this.typeName(other.entity.name);
let type = name;
if (other.entity.type === "system") {
type = `DB["${other.entity.name}"]`;
}
return {
fields: {
[other.reference]: {
required: false,
type: `${type}${listable ? "[]" : ""}`,
},
},
};
}
importsToString(imports: Record<string, string[]>) {
const strings: string[] = [];
for (const [pkg, names] of Object.entries(imports)) {
strings.push(`import type { ${names.join(", ")} } from "${pkg}";`);
}
return strings;
}
commentString(comment?: string, indents = 0, multiline = true) {
if (!comment) return "";
const indent = this.getTab(indents);
if (!multiline) return `${indent}// ${comment}\n`;
return `${indent}/**\n${indent} * ${comment}\n${indent} */\n`;
}
entityToTypeString(
entity: Entity,
opts?: { ignore_fields?: string[]; indent?: number; export?: boolean },
) {
const type = entity.toTypes();
const name = this.typeName(type.name);
const indent = opts?.indent ?? 1;
const min_indent = Math.max(0, indent - 1);
let s = this.commentString(type.comment, min_indent, this.options.entityCommentMultiline);
s += `${opts?.export ? "export " : ""}interface ${name} {\n`;
s += this.fieldTypesToString(type, opts);
// add listable relations
const relations = this.em.relations.relationsOf(entity);
const rel_types = relations.map((r) =>
this.relationToFieldType(r, entity),
) as TEntityTSType[];
for (const rel_type of rel_types) {
s += this.fieldTypesToString(rel_type, {
indent,
});
}
s += `${this.getTab(min_indent)}}`;
return s;
}
toString() {
const strings: string[] = [];
const tables: Record<string, string> = {};
const imports: Record<string, string[]> = {
"bknd/core": ["DB"],
kysely: ["Insertable", "Selectable", "Updateable", "Generated"],
};
// add global types
let g = "declare global {\n";
g += `${this.getTab(1)}type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;\n`;
g += `${this.getTab(1)}type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;\n`;
g += `${this.getTab(1)}type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;\n`;
g += "}";
strings.push(g);
const system_entities = this.em.entities.filter((e) => e.type === "system");
for (const entity of this.em.entities) {
// skip system entities, declare addtional props in the DB interface
if (system_entities.includes(entity)) continue;
const type = entity.toTypes();
if (!type) continue;
this.collectImports(type, imports);
tables[type.name] = this.typeName(type.name);
const s = this.entityToTypeString(entity, {
export: true,
});
strings.push(s);
}
// write tables
let tables_string = "interface Database {\n";
for (const [name, type] of Object.entries(tables)) {
tables_string += `${this.getTab(1)}${name}: ${type};\n`;
}
tables_string += "}";
strings.push(tables_string);
// merge
let merge = `declare module "bknd/core" {\n`;
for (const systemEntity of system_entities) {
const system_fields = Object.keys(systemEntities[systemEntity.name]);
const additional_fields = systemEntity.fields
.filter((f) => !system_fields.includes(f.name) && f.type !== "primary")
.map((f) => f.name);
if (additional_fields.length === 0) continue;
merge += `${this.getTab(1)}${this.entityToTypeString(systemEntity, {
ignore_fields: ["id", ...system_fields],
indent: 2,
})}\n\n`;
}
merge += `${this.getTab(1)}interface DB extends Database {}\n}`;
strings.push(merge);
const final = [this.importsToString(imports).join("\n"), strings.join("\n\n")];
return final.join("\n\n");
}
}

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;
}
}

View File

@@ -302,24 +302,38 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
}
}
private async triggerFindBefore(entity: Entity, options: RepoQuery): Promise<void> {
const event =
options.limit === 1
? Repository.Events.RepositoryFindOneBefore
: Repository.Events.RepositoryFindManyBefore;
await this.emgr.emit(new event({ entity, options }));
}
private async triggerFindAfter(
entity: Entity,
options: RepoQuery,
data: EntityData[],
): Promise<void> {
if (options.limit === 1) {
await this.emgr.emit(
new Repository.Events.RepositoryFindOneAfter({ entity, options, data: data[0]! }),
);
} else {
await this.emgr.emit(
new Repository.Events.RepositoryFindManyAfter({ entity, options, data }),
);
}
}
protected async single(
qb: RepositoryQB,
options: RepoQuery,
): Promise<RepositoryResponse<EntityData>> {
await this.emgr.emit(
new Repository.Events.RepositoryFindOneBefore({ entity: this.entity, options }),
);
await this.triggerFindBefore(this.entity, options);
const { data, ...response } = await this.performQuery(qb);
await this.emgr.emit(
new Repository.Events.RepositoryFindOneAfter({
entity: this.entity,
options,
data: data[0]!,
}),
);
await this.triggerFindAfter(this.entity, options, data);
return { ...response, data: data[0]! };
}
@@ -420,26 +434,16 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
limit: 1,
});
return this.single(qb, options) as any;
return (await this.single(qb, options)) as any;
}
async findMany(_options?: Partial<RepoQuery>): Promise<RepositoryResponse<TBD[TB][]>> {
const { qb, options } = this.buildQuery(_options);
await this.emgr.emit(
new Repository.Events.RepositoryFindManyBefore({ entity: this.entity, options }),
);
await this.triggerFindBefore(this.entity, options);
const res = await this.performQuery(qb);
await this.emgr.emit(
new Repository.Events.RepositoryFindManyAfter({
entity: this.entity,
options,
data: res.data,
}),
);
await this.triggerFindAfter(this.entity, options, res.data);
return res as any;
}

View File

@@ -1,26 +1,26 @@
import { Exception } from "core";
import type { TypeInvalidError } from "core/utils";
import { HttpStatus, type TypeInvalidError } from "core/utils";
import type { Entity } from "./entities";
import type { Field } from "./fields";
export class UnableToConnectException extends Exception {
override name = "UnableToConnectException";
override code = 500;
override code = HttpStatus.INTERNAL_SERVER_ERROR;
}
export class InvalidSearchParamsException extends Exception {
override name = "InvalidSearchParamsException";
override code = 422;
override code = HttpStatus.UNPROCESSABLE_ENTITY;
}
export class TransformRetrieveFailedException extends Exception {
override name = "TransformRetrieveFailedException";
override code = 422;
override code = HttpStatus.UNPROCESSABLE_ENTITY;
}
export class TransformPersistFailedException extends Exception {
override name = "TransformPersistFailedException";
override code = 422;
override code = HttpStatus.UNPROCESSABLE_ENTITY;
static invalidType(property: string, expected: string, given: any) {
const givenValue = typeof given === "object" ? JSON.stringify(given) : given;
@@ -37,7 +37,7 @@ export class TransformPersistFailedException extends Exception {
export class InvalidFieldConfigException extends Exception {
override name = "InvalidFieldConfigException";
override code = 400;
override code = HttpStatus.BAD_REQUEST;
constructor(
field: Field<any, any, any>,
@@ -54,7 +54,7 @@ export class InvalidFieldConfigException extends Exception {
export class EntityNotDefinedException extends Exception {
override name = "EntityNotDefinedException";
override code = 400;
override code = HttpStatus.BAD_REQUEST;
constructor(entity?: Entity | string) {
if (!entity) {
@@ -67,7 +67,7 @@ export class EntityNotDefinedException extends Exception {
export class EntityNotFoundException extends Exception {
override name = "EntityNotFoundException";
override code = 404;
override code = HttpStatus.NOT_FOUND;
constructor(entity: Entity | string, id: any) {
super(

View File

@@ -1,4 +1,4 @@
import type { PrimaryFieldType } from "core";
import { $console, type PrimaryFieldType } from "core";
import { Event, InvalidEventReturn } from "core/events";
import type { Entity, EntityData } from "../entities";
import type { RepoQuery } from "../server/data-query-impl";
@@ -9,6 +9,10 @@ export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityDat
override validate(data: EntityData) {
const { entity } = this.params;
if (!entity.isValidData(data, "create")) {
$console.warn("MutatorInsertBefore.validate: invalid", {
entity: entity.name,
data,
});
throw new InvalidEventReturn("EntityData", "invalid");
}
@@ -36,13 +40,18 @@ export class MutatorUpdateBefore extends Event<
static override slug = "mutator-update-before";
override validate(data: EntityData) {
const { entity, ...rest } = this.params;
const { entity, entityId } = this.params;
if (!entity.isValidData(data, "update")) {
$console.warn("MutatorUpdateBefore.validate: invalid", {
entity: entity.name,
entityId,
data,
});
throw new InvalidEventReturn("EntityData", "invalid");
}
return this.clone({
...rest,
entityId,
entity,
data,
});

View File

@@ -1,7 +1,9 @@
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tb from "@sinclair/typebox";
const { Type } = tb;
export const booleanFieldConfigSchema = Type.Composite([
Type.Object({
@@ -47,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();
@@ -87,4 +88,11 @@ export class BooleanField<Required extends true | false = false> extends Field<
override toJsonSchema() {
return this.toSchemaWrapIfRequired(Type.Boolean({ default: this.getDefault() }));
}
override toType() {
return {
...super.toType(),
type: "boolean",
};
}
}

View File

@@ -1,11 +1,14 @@
import { type Static, StringEnum, Type, dayjs } from "core/utils";
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";
import type { TFieldTSType } from "data/entities/EntityTypescript";
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()),
@@ -51,13 +54,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();
}
@@ -67,15 +68,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;
}
}
@@ -84,7 +82,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;
}
}
@@ -97,8 +95,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;
}
}
@@ -117,7 +114,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;
@@ -136,7 +132,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":
@@ -150,4 +145,11 @@ export class DateField<Required extends true | false = false> extends Field<
override toJsonSchema() {
return this.toSchemaWrapIfRequired(Type.String({ default: this.getDefault() }));
}
override toType(): TFieldTSType {
return {
...super.toType(),
type: "Date | string",
};
}
}

View File

@@ -1,7 +1,10 @@
import { Const, type Static, StringEnum, StringRecord, Type } from "core/utils";
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";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
export const enumFieldConfigSchema = Type.Composite(
[
@@ -53,10 +56,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`);
}
@@ -69,10 +68,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 }));
}
@@ -146,4 +141,14 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
}),
);
}
override toType(): TFieldTSType {
const union = this.getOptions().map(({ value }) =>
typeof value === "string" ? `"${value}"` : value,
);
return {
...super.toType(),
type: union.length > 0 ? union.join(" | ") : "string",
};
}
}

View File

@@ -4,13 +4,15 @@ import {
type Static,
StringEnum,
type TSchema,
Type,
TypeInvalidError,
} from "core/utils";
import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
import type { EntityManager } from "../entities";
import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors";
import type { FieldSpec } from "data/connection/Connection";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
// @todo: contexts need to be reworked
// e.g. "table" is irrelevant, because if read is not given, it fails
@@ -184,12 +186,14 @@ export abstract class Field<
};
}
// @todo: add field level validation
isValid(value: any, context: TActionContext): boolean {
if (value) {
if (typeof value !== "undefined") {
return this.isFillable(context);
} else {
} else if (context === "create") {
return !this.isRequired();
}
return true;
}
/**
@@ -232,6 +236,14 @@ export abstract class Field<
return this.toSchemaWrapIfRequired(Type.Any());
}
toType(): TFieldTSType {
return {
required: this.isRequired(),
comment: this.getDescription(),
type: "any",
};
}
toJSON() {
return {
// @todo: current workaround because of fixed string type

View File

@@ -1,7 +1,10 @@
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
export const jsonFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]);
@@ -82,7 +85,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)) {
@@ -97,4 +99,11 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
return JSON.stringify(value);
}
override toType(): TFieldTSType {
return {
...super.toType(),
type: "any",
};
}
}

View File

@@ -1,8 +1,11 @@
import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema";
import { Default, FromSchema, type Static, Type } from "core/utils";
import { Default, FromSchema, objectToJsLiteral, type Static } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
export const jsonSchemaFieldConfigSchema = Type.Composite(
[
@@ -46,22 +49,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;
}
@@ -89,7 +86,6 @@ export class JsonSchemaField<
try {
return Default(FromSchema(this.getJsonSchema()), {});
} catch (e) {
//console.error("jsonschema:transformRetrieve", e);
return null;
}
} else if (this.hasDefault()) {
@@ -107,13 +103,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();
@@ -130,4 +122,12 @@ export class JsonSchemaField<
}),
);
}
override toType(): TFieldTSType {
return {
...super.toType(),
import: [{ package: "json-schema-to-ts", name: "FromSchema" }],
type: `FromSchema<${objectToJsLiteral(this.getJsonSchema(), 2, 1)}>`,
};
}
}

View File

@@ -1,7 +1,10 @@
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
export const numberFieldConfigSchema = Type.Composite(
[
@@ -100,4 +103,11 @@ export class NumberField<Required extends true | false = false> extends Field<
}),
);
}
override toType(): TFieldTSType {
return {
...super.toType(),
type: "number",
};
}
}

View File

@@ -1,6 +1,9 @@
import { config } from "core";
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
export const primaryFieldConfigSchema = Type.Composite([
Type.Omit(baseFieldConfigSchema, ["required"]),
@@ -46,4 +49,13 @@ export class PrimaryField<Required extends true | false = false> extends Field<
override toJsonSchema() {
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined }));
}
override toType(): TFieldTSType {
return {
...super.toType(),
required: true,
import: [{ package: "kysely", name: "Generated" }],
type: "Generated<number>",
};
}
}

View File

@@ -1,7 +1,9 @@
import { type Static, Type } from "core/utils";
import type { EntityManager } from "data";
import type { Static } from "core/utils";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, baseFieldConfigSchema } from "./Field";
import * as tb from "@sinclair/typebox";
const { Type } = tb;
export const textFieldConfigSchema = Type.Composite(
[
@@ -119,4 +121,11 @@ export class TextField<Required extends true | false = false> extends Field<
}),
);
}
override toType() {
return {
...super.toType(),
type: "string",
};
}
}

View File

@@ -1,5 +1,7 @@
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "./Field";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export const virtualFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]);

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,

View File

@@ -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,
};
}

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,

View File

@@ -1,4 +1,4 @@
import { type Static, Type, parse } from "core/utils";
import { type Static, parse } from "core/utils";
import type { ExpressionBuilder, SelectQueryBuilder } from "kysely";
import type { Entity, EntityData, EntityManager } from "../entities";
import {
@@ -8,19 +8,15 @@ import {
} from "../relations";
import type { RepoQuery } from "../server/data-query-impl";
import type { RelationType } from "./relation-types";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
const directions = ["source", "target"] as const;
export type TDirection = (typeof directions)[number];
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
@@ -34,7 +30,7 @@ export abstract class EntityRelation<
// @todo: add unit tests
// allowed directions, used in RelationAccessor for visibility
directions: ("source" | "target")[] = ["source", "target"];
directions: TDirection[] = ["source", "target"];
static schema = Type.Object({
mappedBy: Type.Optional(Type.String()),
@@ -109,6 +105,10 @@ export abstract class EntityRelation<
);
}
self(entity: Entity | string): EntityRelationAnchor {
return this.other(entity).entity.name === this.source.entity.name ? this.target : this.source;
}
ref(reference: string): EntityRelationAnchor {
return this.source.reference === reference ? this.source : this.target;
}
@@ -165,7 +165,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;
}

View File

@@ -1,12 +1,14 @@
import { type Static, Type } from "core/utils";
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";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export type ManyToManyRelationConfig = Static<typeof ManyToManyRelation.schema>;
@@ -46,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) {

View File

@@ -1,14 +1,16 @@
import type { PrimaryFieldType } from "core";
import { snakeToPascalWithSpaces } from "core/utils";
import { type Static, Type } from "core/utils";
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";
import { type RelationType, RelationTypes } from "./relation-types";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
/**
* Source entity receives the mapping field
@@ -125,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,

View File

@@ -1,4 +1,4 @@
import { type Static, Type } from "core/utils";
import type { Static } from "core/utils";
import type { ExpressionBuilder } from "kysely";
import type { Entity, EntityManager } from "../entities";
import { NumberField, TextField } from "../fields";
@@ -6,6 +6,8 @@ import type { RepoQuery } from "../server/data-query-impl";
import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { type RelationType, RelationTypes } from "./relation-types";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export type PolymorphicRelationConfig = Static<typeof PolymorphicRelation.schema>;

View File

@@ -1,8 +1,11 @@
import { type Static, StringEnum, Type } from "core/utils";
import { type Static, StringEnum } from "core/utils";
import type { EntityManager } from "../entities";
import { Field, baseFieldConfigSchema } from "../fields";
import type { EntityRelation } from "./EntityRelation";
import type { EntityRelationAnchor } from "./EntityRelationAnchor";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;
const CASCADES = ["cascade", "set null", "set default", "restrict", "no action"] as const;
@@ -15,11 +18,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 };
@@ -31,16 +29,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,
@@ -50,7 +38,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,
@@ -96,4 +84,11 @@ export class RelationField extends Field<RelationFieldConfig> {
}),
);
}
override toType(): TFieldTSType {
return {
...super.toType(),
type: "number",
};
}
}

View File

@@ -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.`);
}

View File

@@ -1,14 +1,8 @@
import type { TThis } from "@sinclair/typebox";
import {
type SchemaOptions,
type Static,
type StaticDecode,
StringEnum,
Type,
Value,
isObject,
} from "core/utils";
import { type SchemaOptions, type StaticDecode, StringEnum, Value, isObject } from "core/utils";
import { WhereBuilder, type WhereQuery } from "../entities";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
const NumberOrString = (options: SchemaOptions = {}) =>
Type.Transform(Type.Union([Type.Number(), Type.String()], options))

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 };