diff --git a/app/__test__/data/mutation.simple.test.ts b/app/__test__/data/mutation.simple.test.ts index dd385af..b54bca8 100644 --- a/app/__test__/data/mutation.simple.test.ts +++ b/app/__test__/data/mutation.simple.test.ts @@ -16,7 +16,7 @@ describe("Mutator simple", async () => { new TextField("label", { required: true, minLength: 1 }), new NumberField("count", { default_value: 0 }) ]); - const em = new EntityManager([items], connection); + const em = new EntityManager([items], connection); await em.connection.kysely.schema .createTable("items") @@ -175,4 +175,18 @@ describe("Mutator simple", async () => { { id: 8, label: "keep", count: 0 } ]); }); + + test("insertMany", async () => { + const oldCount = (await em.repo(items).count()).count; + const inserts = [{ label: "insert 1" }, { label: "insert 2" }]; + const { data } = await em.mutator(items).insertMany(inserts); + + expect(data.length).toBe(2); + expect(data.map((d) => ({ label: d.label }))).toEqual(inserts); + const newCount = (await em.repo(items).count()).count; + expect(newCount).toBe(oldCount + inserts.length); + + const { data: data2 } = await em.repo(items).findMany(); + expect(data2).toEqual(data); + }); }); diff --git a/app/__test__/data/prototype.test.ts b/app/__test__/data/prototype.test.ts index 9d3eebd..f905775 100644 --- a/app/__test__/data/prototype.test.ts +++ b/app/__test__/data/prototype.test.ts @@ -13,6 +13,7 @@ import { PolymorphicRelation, TextField } from "../../src/data"; +import { DummyConnection } from "../../src/data/connection/DummyConnection"; import { FieldPrototype, type FieldSchema, @@ -21,6 +22,7 @@ import { boolean, date, datetime, + em, entity, enumm, json, @@ -272,4 +274,29 @@ describe("prototype", () => { const obj: Schema = {} as any; }); + + test("schema", async () => { + const _em = em( + { + posts: entity("posts", { name: text() }), + comments: entity("comments", { some: text() }) + }, + (relation, { posts, comments }) => { + relation(posts).manyToOne(comments); + } + ); + + type LocalDb = (typeof _em)["DB"]; + + const es = [ + new Entity("posts", [new TextField("name")]), + new Entity("comments", [new TextField("some")]) + ]; + const _em2 = new EntityManager(es, new DummyConnection(), [ + new ManyToOneRelation(es[0], es[1]) + ]); + + // @ts-ignore + expect(_em2.toJSON()).toEqual(_em.toJSON()); + }); }); diff --git a/app/__test__/data/specs/Mutator.spec.ts b/app/__test__/data/specs/Mutator.spec.ts index 04bd8a3..5552543 100644 --- a/app/__test__/data/specs/Mutator.spec.ts +++ b/app/__test__/data/specs/Mutator.spec.ts @@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => { new TextField("hidden", { hidden: true }), new TextField("not_fillable", { fillable: false }) ]); - const em = new EntityManager([entity], dummyConnection); + const em = new EntityManager([entity], dummyConnection); await em.schema().sync({ force: true }); const payload = { label: "item 1", count: 1 }; @@ -61,7 +61,7 @@ describe("[data] Mutator (ManyToOne)", async () => { const posts = new Entity("posts", [new TextField("title")]); const users = new Entity("users", [new TextField("username")]); const relations = [new ManyToOneRelation(posts, users)]; - const em = new EntityManager([posts, users], dummyConnection, relations); + const em = new EntityManager([posts, users], dummyConnection, relations); await em.schema().sync({ force: true }); test("RelationMutator", async () => { @@ -192,7 +192,7 @@ describe("[data] Mutator (OneToOne)", async () => { const users = new Entity("users", [new TextField("username")]); const settings = new Entity("settings", [new TextField("theme")]); const relations = [new OneToOneRelation(users, settings)]; - const em = new EntityManager([users, settings], dummyConnection, relations); + const em = new EntityManager([users, settings], dummyConnection, relations); await em.schema().sync({ force: true }); test("insertOne: missing ref", async () => { @@ -276,7 +276,7 @@ describe("[data] Mutator (ManyToMany)", async () => { describe("[data] Mutator (Events)", async () => { const entity = new Entity("test", [new TextField("label")]); - const em = new EntityManager([entity], dummyConnection); + const em = new EntityManager([entity], dummyConnection); await em.schema().sync({ force: true }); const events = new Map(); diff --git a/app/src/data/connection/DummyConnection.ts b/app/src/data/connection/DummyConnection.ts new file mode 100644 index 0000000..451575d --- /dev/null +++ b/app/src/data/connection/DummyConnection.ts @@ -0,0 +1,7 @@ +import { Connection } from "./Connection"; + +export class DummyConnection extends Connection { + constructor() { + super(undefined as any); + } +} diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index 53b4cb2..3f81c6e 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -276,4 +276,39 @@ export class Mutator> { + const entity = this.entity; + if (entity.type === "system" && this.__unstable_disable_system_entity_creation) { + throw new Error(`Creation of system entity "${entity.name}" is disabled`); + } + + const validated: any[] = []; + for (const row of data) { + const validatedData = { + ...entity.getDefaultObject(), + ...(await this.getValidatedData(row, "create")) + }; + + // check if required fields are present + const required = entity.getRequiredFields(); + for (const field of required) { + if ( + typeof validatedData[field.name] === "undefined" || + validatedData[field.name] === null + ) { + throw new Error(`Field "${field.name}" is required`); + } + } + + validated.push(validatedData); + } + + const query = this.conn + .insertInto(entity.name) + .values(validated) + .returning(entity.getSelect()); + + return (await this.many(query)) as any; + } } diff --git a/app/src/data/prototype/index.ts b/app/src/data/prototype/index.ts index 8bdd07b..0e44138 100644 --- a/app/src/data/prototype/index.ts +++ b/app/src/data/prototype/index.ts @@ -1,3 +1,5 @@ +import { DummyConnection } from "data/connection/DummyConnection"; +import { EntityManager } from "data/entities/EntityManager"; import type { Generated } from "kysely"; import { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField"; import { @@ -7,6 +9,7 @@ import { type DateFieldConfig, Entity, type EntityConfig, + type EntityRelation, EnumField, type EnumFieldConfig, type Field, @@ -240,6 +243,57 @@ export function relation(local: Local) { }; } +class EntityManagerPrototype> extends EntityManager< + Schema +> { + constructor( + public __entities: Entities, + relations: EntityRelation[] + ) { + super(Object.values(__entities), new DummyConnection(), relations); + } +} + +export function em>( + entities: Entities, + schema?: (rel: typeof relation, entities: Entities) => void +) { + const relations: EntityRelation[] = []; + const relationProxy = (local: Entity) => { + return new Proxy(relation(local), { + get(target, prop) { + if (typeof target[prop] === "function") { + return (...args: any[]) => { + const result = target[prop](...args); + relations.push(result); + return result; + }; + } + return target[prop]; + } + }); + }; + + if (schema) { + schema(relationProxy, entities); + } + + const e = new EntityManagerPrototype(entities, relations); + return { + DB: e.__entities as unknown as Schemas, + entities: e.__entities, + relations, + indices: [], + toJSON: () => { + return e.toJSON() as unknown as { + entities: Schemas; + relations: EntityRelation[]; + indices: any[]; + }; + } + }; +} + export type InferEntityFields = T extends Entity ? { [K in keyof Fields]: Fields[K] extends { _type: infer Type; _required: infer Required } @@ -291,18 +345,9 @@ export type InferField = Field extends { _type: infer Type; _required: in : Type | undefined : never; -const n = number(); -type T2 = InferField; - -const users = entity("users", { - name: text(), - email: text(), - created_at: datetime(), - updated_at: datetime() -}); -type TUsersFields = InferEntityFields; -type TUsers = Schema; -type TUsers2 = Simplify>>; +export type Schemas> = { + [K in keyof T]: Schema; +}; export type InsertSchema = Simplify>>; export type Schema = Simplify<{ id: Generated } & InsertSchema>;