diff --git a/app/__test__/data/mutation.simple.test.ts b/app/__test__/data/mutation.simple.test.ts index b54bca8..ac19935 100644 --- a/app/__test__/data/mutation.simple.test.ts +++ b/app/__test__/data/mutation.simple.test.ts @@ -186,7 +186,7 @@ describe("Mutator simple", async () => { const newCount = (await em.repo(items).count()).count; expect(newCount).toBe(oldCount + inserts.length); - const { data: data2 } = await em.repo(items).findMany(); + const { data: data2 } = await em.repo(items).findMany({ offset: oldCount }); expect(data2).toEqual(data); }); }); diff --git a/app/__test__/data/prototype.test.ts b/app/__test__/data/prototype.test.ts index f905775..8b12aa4 100644 --- a/app/__test__/data/prototype.test.ts +++ b/app/__test__/data/prototype.test.ts @@ -3,6 +3,7 @@ import { BooleanField, DateField, Entity, + EntityIndex, EntityManager, EnumField, JsonField, @@ -278,23 +279,32 @@ describe("prototype", () => { test("schema", async () => { const _em = em( { - posts: entity("posts", { name: text() }), - comments: entity("comments", { some: text() }) + posts: entity("posts", { name: text(), slug: text().required() }), + comments: entity("comments", { some: text() }), + users: entity("users", { email: text() }) }, - (relation, { posts, comments }) => { - relation(posts).manyToOne(comments); + ({ relation, index }, { posts, comments, users }) => { + relation(posts).manyToOne(comments).manyToOne(users); + index(posts).on(["name"]).on(["slug"], true); } ); type LocalDb = (typeof _em)["DB"]; const es = [ - new Entity("posts", [new TextField("name")]), - new Entity("comments", [new TextField("some")]) + new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]), + new Entity("comments", [new TextField("some")]), + new Entity("users", [new TextField("email")]) ]; - const _em2 = new EntityManager(es, new DummyConnection(), [ - new ManyToOneRelation(es[0], es[1]) - ]); + const _em2 = new EntityManager( + es, + new DummyConnection(), + [new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])], + [ + new EntityIndex(es[0], [es[0].field("name")!]), + new EntityIndex(es[0], [es[0].field("slug")!], true) + ] + ); // @ts-ignore expect(_em2.toJSON()).toEqual(_em.toJSON()); diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index 0a285e7..579ffb2 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -220,7 +220,8 @@ export class Entity< readOnly: !field.isFillable("update") ? true : undefined, writeOnly: !field.isFillable("create") ? true : undefined, ...field.toJsonSchema() - })) + })), + { additionalProperties: false } ); return clean ? JSON.parse(JSON.stringify(schema)) : schema; diff --git a/app/src/data/fields/TextField.ts b/app/src/data/fields/TextField.ts index 6314618..6dc17d3 100644 --- a/app/src/data/fields/TextField.ts +++ b/app/src/data/fields/TextField.ts @@ -104,6 +104,12 @@ export class TextField extends Field< ); } + if (this.config.pattern && value && !new RegExp(this.config.pattern).test(value)) { + throw new TransformPersistFailedException( + `Field "${this.name}" must match the pattern ${this.config.pattern}` + ); + } + return value; } diff --git a/app/src/data/prototype/index.ts b/app/src/data/prototype/index.ts index 37ca5b2..e9e868f 100644 --- a/app/src/data/prototype/index.ts +++ b/app/src/data/prototype/index.ts @@ -10,6 +10,7 @@ import { type DateFieldConfig, Entity, type EntityConfig, + EntityIndex, type EntityRelation, EnumField, type EnumFieldConfig, @@ -244,47 +245,83 @@ export function relation(local: Local) { }; } +export function index(entity: E) { + return { + on: (fields: (keyof InsertSchema)[], unique?: boolean) => { + const _fields = fields.map((f) => { + const field = entity.field(f as any); + if (!field) { + throw new Error(`Field "${String(f)}" not found on entity "${entity.name}"`); + } + return field; + }); + return new EntityIndex(entity, _fields, unique); + } + }; +} + class EntityManagerPrototype> extends EntityManager< Schema > { constructor( public __entities: Entities, - relations: EntityRelation[] + relations: EntityRelation[] = [], + indices: EntityIndex[] = [] ) { - super(Object.values(__entities), new DummyConnection(), relations); + super(Object.values(__entities), new DummyConnection(), relations, indices); } } +type Chained any, Rt = ReturnType> = ( + e: E +) => { + [K in keyof Rt]: Rt[K] extends (...args: any[]) => any + ? (...args: Parameters) => Rt + : never; +}; + export function em>( entities: Entities, - schema?: (rel: typeof relation, entities: Entities) => void + schema?: ( + fns: { relation: Chained; index: Chained }, + entities: Entities + ) => void ) { const relations: EntityRelation[] = []; - const relationProxy = (local: Entity) => { - return new Proxy(relation(local), { + const indices: EntityIndex[] = []; + + const relationProxy = (e: Entity) => { + return new Proxy(relation(e), { get(target, prop) { - if (typeof target[prop] === "function") { - return (...args: any[]) => { - const result = target[prop](...args); - relations.push(result); - return result; - }; - } - return target[prop]; + return (...args: any[]) => { + relations.push(target[prop](...args)); + return relationProxy(e); + }; } - }); + }) as any; + }; + + const indexProxy = (e: Entity) => { + return new Proxy(index(e), { + get(target, prop) { + return (...args: any[]) => { + indices.push(target[prop](...args)); + return indexProxy(e); + }; + } + }) as any; }; if (schema) { - schema(relationProxy, entities); + schema({ relation: relationProxy, index: indexProxy }, entities); } - const e = new EntityManagerPrototype(entities, relations); + const e = new EntityManagerPrototype(entities, relations, indices); return { DB: e.__entities as unknown as Schemas, entities: e.__entities, relations, - indices: [], + indices, toJSON: () => e.toJSON() as unknown as Pick };