mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 20:37:21 +00:00
public commit
This commit is contained in:
235
app/__test__/data/DataController.spec.ts
Normal file
235
app/__test__/data/DataController.spec.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
describe("[data] DataController", async () => {
|
||||
test("repoResult", async () => {
|
||||
const em = new EntityManager<any>([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.repoResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
}
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
},
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
test("mutatorResult", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.mutatorResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
describe("getController", async () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("bio")
|
||||
]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const em = new EntityManager([users, posts], dummyConnection, [
|
||||
new ManyToOneRelation(posts, users)
|
||||
]);
|
||||
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const fixtures = {
|
||||
users: [
|
||||
{ name: "foo", bio: "bar" },
|
||||
{ name: "bar", bio: null },
|
||||
{ name: "baz", bio: "!!!" }
|
||||
],
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 }
|
||||
]
|
||||
};
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
test("entityExists", async () => {
|
||||
expect(controller.entityExists("users")).toBe(true);
|
||||
expect(controller.entityExists("posts")).toBe(true);
|
||||
expect(controller.entityExists("settings")).toBe(false);
|
||||
});
|
||||
|
||||
// @todo: update
|
||||
test("/ (get info)", async () => {
|
||||
const res = await app.request("/");
|
||||
const data = (await res.json()) as any;
|
||||
const entities = Object.keys(data.entities);
|
||||
const relations = Object.values(data.relations).map((r: any) => r.type);
|
||||
|
||||
expect(entities).toEqual(["users", "posts"]);
|
||||
expect(relations).toEqual(["n:1"]);
|
||||
});
|
||||
|
||||
test("/:entity (insert one)", async () => {
|
||||
//console.log("app.routes", app.routes);
|
||||
// create users
|
||||
for await (const _user of fixtures.users) {
|
||||
const res = await app.request("/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user)
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual(_user);
|
||||
}
|
||||
|
||||
// create posts
|
||||
for await (const _post of fixtures.posts) {
|
||||
const res = await app.request("/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post)
|
||||
});
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual(_post);
|
||||
}
|
||||
});
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
const res = await app.request("/users");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(3);
|
||||
expect(data.data.length).toBe(3);
|
||||
expect(data.data[0].name).toBe("foo");
|
||||
});
|
||||
|
||||
test("/:entity/query (func query)", async () => {
|
||||
const res = await app.request("/users/query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
where: { bio: { $isnull: 1 } }
|
||||
})
|
||||
});
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("bar");
|
||||
});
|
||||
|
||||
test("/:entity (read many, paginated)", async () => {
|
||||
const res = await app.request("/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("baz");
|
||||
});
|
||||
|
||||
test("/:entity/:id (read one)", async () => {
|
||||
const res = await app.request("/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||
});
|
||||
|
||||
test("/:entity (update one)", async () => {
|
||||
const res = await app.request("/users/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" })
|
||||
});
|
||||
const { data } = (await res.json()) as MutatorResponse;
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||
});
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
|
||||
expect(data.meta.total).toBe(2);
|
||||
expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].content).toBe("post 1");
|
||||
});
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/posts/2", {
|
||||
method: "DELETE"
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
92
app/__test__/data/data-query-impl.spec.ts
Normal file
92
app/__test__/data/data-query-impl.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { QueryObject } from "ufo";
|
||||
import { WhereBuilder, type WhereQuery } from "../../src/data/entities/query/WhereBuilder";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const t = "t";
|
||||
describe("data-query-impl", () => {
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
const kysely = c.dummyConnection.kysely;
|
||||
return kysely.selectFrom(t).selectAll();
|
||||
}
|
||||
function compile(q: QueryObject) {
|
||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
|
||||
test("single validation", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
|
||||
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
|
||||
[{ val: { $isnull: 1 } }, '"val" is null', []],
|
||||
[{ val: { $isnull: true } }, '"val" is null', []],
|
||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("multiple validations", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||
|
||||
// multiple properties
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
|
||||
// or constructs
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
'("val1" = ? or "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]]
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
const { sql, parameters } = compile(query);
|
||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||
expect(parameters).toEqual(expectedParams);
|
||||
}
|
||||
});
|
||||
|
||||
test("keys", () => {
|
||||
const tests: [WhereQuery, string[]][] = [
|
||||
// multiple constraints per property
|
||||
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
|
||||
|
||||
// multiple properties
|
||||
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
|
||||
|
||||
// or constructs
|
||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
|
||||
];
|
||||
|
||||
for (const [query, expectedKeys] of tests) {
|
||||
const keys = WhereBuilder.getPropertyNames(query);
|
||||
expect(keys).toEqual(expectedKeys);
|
||||
}
|
||||
});
|
||||
});
|
||||
113
app/__test__/data/data.test.ts
Normal file
113
app/__test__/data/data.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("some tests", async () => {
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
const connection = dummyConnection;
|
||||
|
||||
const users = new Entity("users", [
|
||||
new TextField("username", { required: true, default_value: "nobody" }),
|
||||
new TextField("email", { max_length: 3 })
|
||||
]);
|
||||
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at"),
|
||||
new NumberField("likes", { default_value: 0 })
|
||||
]);
|
||||
|
||||
const em = new EntityManager([users, posts], connection);
|
||||
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("findId", async () => {
|
||||
const query = await em.repository(users).findId(1);
|
||||
/*const { result, total, count, time } = query;
|
||||
console.log("query", query.result, {
|
||||
result,
|
||||
total,
|
||||
count,
|
||||
time,
|
||||
});*/
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?'
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.result).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany", async () => {
|
||||
const query = await em.repository(users).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?'
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany with number", async () => {
|
||||
const query = await em.repository(posts).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?'
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
});
|
||||
|
||||
test("try adding an existing field name", async () => {
|
||||
expect(() => {
|
||||
new Entity("users", [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("email") // not throwing, it's just being ignored
|
||||
]);
|
||||
}).toBeDefined();
|
||||
|
||||
expect(() => {
|
||||
new Entity("users", [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
// field config differs, will throw
|
||||
new TextField("email", { required: true })
|
||||
]);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new Entity("users", [
|
||||
new PrimaryField(),
|
||||
new TextField("username"),
|
||||
new TextField("email")
|
||||
]);
|
||||
}).toBeDefined();
|
||||
});
|
||||
|
||||
test("try adding duplicate entities", async () => {
|
||||
const entity = new Entity("users", [new TextField("username")]);
|
||||
const entity2 = new Entity("users", [new TextField("userna1me")]);
|
||||
|
||||
expect(() => {
|
||||
// will not throw, just ignored
|
||||
new EntityManager([entity, entity], connection);
|
||||
}).toBeDefined();
|
||||
|
||||
expect(() => {
|
||||
// the config differs, so it throws
|
||||
new EntityManager([entity, entity2], connection);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
35
app/__test__/data/helper.ts
Normal file
35
app/__test__/data/helper.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
afterAllCleanup: () => Promise<boolean>;
|
||||
} {
|
||||
const DB_NAME = memory ? ":memory:" : `${Math.random().toString(36).substring(7)}.db`;
|
||||
const dummyDb = new Database(DB_NAME);
|
||||
|
||||
return {
|
||||
dummyDb,
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getDummyConnection(memory: boolean = true) {
|
||||
const { dummyDb, afterAllCleanup } = getDummyDatabase(memory);
|
||||
const dummyConnection = new SqliteLocalConnection(dummyDb);
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
};
|
||||
}
|
||||
|
||||
export function getLocalLibsqlConnection() {
|
||||
return { url: "http://127.0.0.1:8080" };
|
||||
}
|
||||
50
app/__test__/data/mutation.relation.test.ts
Normal file
50
app/__test__/data/mutation.relation.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("Mutator relation", async () => {
|
||||
const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
//const connection = getCreds("DB_DATA");
|
||||
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content", { default_value: "..." }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
]);
|
||||
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
|
||||
const relations = [new ManyToOneRelation(posts, users)];
|
||||
|
||||
const em = new EntityManager([posts, users], connection, relations);
|
||||
|
||||
const schema = new SchemaManager(em);
|
||||
await schema.sync({ force: true });
|
||||
|
||||
test("add users", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
await em.mutator(users).insertOne({ username: "user2" });
|
||||
|
||||
// create some posts
|
||||
await em.mutator(posts).insertOne({ title: "post1", content: "content1" });
|
||||
|
||||
// expect to throw
|
||||
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
|
||||
|
||||
expect(
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
145
app/__test__/data/mutation.simple.test.ts
Normal file
145
app/__test__/data/mutation.simple.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("Mutator simple", async () => {
|
||||
const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
//const connection = getCreds("DB_DATA");
|
||||
|
||||
const items = new Entity("items", [
|
||||
new TextField("label", { required: true, minLength: 1 }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
]);
|
||||
const em = new EntityManager([items], connection);
|
||||
|
||||
await em.connection.kysely.schema
|
||||
.createTable("items")
|
||||
.ifNotExists()
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("label", "text")
|
||||
.addColumn("count", "integer")
|
||||
.execute();
|
||||
|
||||
test("insert single row", async () => {
|
||||
const mutation = await em.mutator(items).insertOne({
|
||||
label: "test",
|
||||
count: 1
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"'
|
||||
);
|
||||
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||
|
||||
const query = await em.repository(items).findMany({
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
});
|
||||
|
||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
});
|
||||
|
||||
test("update inserted row", async () => {
|
||||
const query = await em.repository(items).findMany({
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
});
|
||||
const id = query.data![0].id as number;
|
||||
|
||||
const mutation = await em.mutator(items).updateOne(id, {
|
||||
label: "new label",
|
||||
count: 100
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"'
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
});
|
||||
|
||||
test("delete updated row", async () => {
|
||||
const query = await em.repository(items).findMany({
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
});
|
||||
|
||||
const id = query.data![0].id as number;
|
||||
const mutation = await em.mutator(items).deleteOne(id);
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"'
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
const query2 = await em.repository(items).findId(id);
|
||||
expect(query2.result.length).toBe(0);
|
||||
});
|
||||
|
||||
test("validation: insert incomplete row", async () => {
|
||||
const incompleteCreate = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
//label: "test",
|
||||
count: 1
|
||||
});
|
||||
|
||||
expect(incompleteCreate()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("validation: insert invalid row", async () => {
|
||||
const invalidCreate1 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: 111, // this should work
|
||||
count: "1" // this should fail
|
||||
});
|
||||
|
||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||
|
||||
const invalidCreate2 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: "", // this should fail
|
||||
count: 1
|
||||
});
|
||||
|
||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||
});
|
||||
|
||||
test("test default value", async () => {
|
||||
const res = await em.mutator(items).insertOne({ label: "yo" });
|
||||
|
||||
expect(res.data.count).toBe(0);
|
||||
});
|
||||
|
||||
test("deleteMany", async () => {
|
||||
await em.mutator(items).insertOne({ label: "keep" });
|
||||
await em.mutator(items).insertOne({ label: "delete" });
|
||||
await em.mutator(items).insertOne({ label: "delete" });
|
||||
|
||||
const data = (await em.repository(items).findMany()).data;
|
||||
//console.log(data);
|
||||
|
||||
await em.mutator(items).deleteMany({ label: "delete" });
|
||||
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||
//console.log((await em.repository(items).findMany()).data);
|
||||
|
||||
await em.mutator(items).deleteMany();
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||
|
||||
//expect(res.data.count).toBe(0);
|
||||
});
|
||||
});
|
||||
96
app/__test__/data/polymorphic.test.ts
Normal file
96
app/__test__/data/polymorphic.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
const expect = (value: any) => bunExpect(stripMark(value));
|
||||
|
||||
describe("Polymorphic", async () => {
|
||||
test("Simple", async () => {
|
||||
const categories = new Entity("categories", [new TextField("name")]);
|
||||
const media = new Entity("media", [new TextField("path")]);
|
||||
|
||||
const entities = [media, categories];
|
||||
const relation = new PolymorphicRelation(categories, media, { mappedBy: "image" });
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, [relation]);
|
||||
|
||||
expect(em.relationsOf(categories.name).map((r) => r.toJSON())[0]).toEqual({
|
||||
type: "poly",
|
||||
source: "categories",
|
||||
target: "media",
|
||||
config: {
|
||||
mappedBy: "image"
|
||||
}
|
||||
});
|
||||
// media should not see categories
|
||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media"
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"image"
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
// expect that polymorphic fields are added to media
|
||||
expect(media.getFields().map((f) => f.name)).toEqual([
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
]);
|
||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||
});
|
||||
|
||||
test("Multiple to the same", async () => {
|
||||
const categories = new Entity("categories", [new TextField("name")]);
|
||||
const media = new Entity("media", [new TextField("path")]);
|
||||
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, [single, multiple]);
|
||||
|
||||
// media should not see categories
|
||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media",
|
||||
"media"
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"single",
|
||||
"multiple"
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
// expect that polymorphic fields are added to media
|
||||
expect(media.getFields().map((f) => f.name)).toEqual([
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
]);
|
||||
});
|
||||
});
|
||||
267
app/__test__/data/prototype.test.ts
Normal file
267
app/__test__/data/prototype.test.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { MediaField } from "../../src";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
Entity,
|
||||
EnumField,
|
||||
JsonField,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
type InsertSchema,
|
||||
type Schema,
|
||||
boolean,
|
||||
date,
|
||||
datetime,
|
||||
entity,
|
||||
enumm,
|
||||
json,
|
||||
media,
|
||||
medium,
|
||||
number,
|
||||
relation,
|
||||
text
|
||||
} from "../../src/data/prototype";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
const fieldPrototype = new FieldPrototype("text", {}, false);
|
||||
//console.log("field", fieldPrototype, fieldPrototype.getField("name"));
|
||||
/*const user = entity("users", {
|
||||
name: text().required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required(),
|
||||
});
|
||||
|
||||
console.log("user", user);*/
|
||||
});
|
||||
|
||||
test("...2", async () => {
|
||||
const user = entity("users", {
|
||||
name: text().required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
});
|
||||
|
||||
//console.log("user", user.toJSON());
|
||||
});
|
||||
|
||||
test("...3", async () => {
|
||||
const user = entity("users", {
|
||||
name: text({ default_value: "hello" }).required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
});
|
||||
|
||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||
|
||||
//console.log("user2", user.toJSON());
|
||||
});
|
||||
|
||||
test("Post example", async () => {
|
||||
const posts1 = new Entity("posts", [
|
||||
new TextField("title", { required: true }),
|
||||
new TextField("content"),
|
||||
new DateField("created_at", {
|
||||
type: "datetime"
|
||||
}),
|
||||
new MediaField("images", { entity: "posts" }),
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||
]);
|
||||
|
||||
const posts2 = entity("posts", {
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
});
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
|
||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
});
|
||||
|
||||
test("test example", async () => {
|
||||
const test = new Entity("test", [
|
||||
new TextField("name"),
|
||||
new BooleanField("checked", { default_value: false }),
|
||||
new NumberField("count"),
|
||||
new DateField("created_at"),
|
||||
new DateField("updated_at", { type: "datetime" }),
|
||||
new TextField("description"),
|
||||
new EnumField("status", {
|
||||
options: {
|
||||
type: "objects",
|
||||
values: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}
|
||||
}),
|
||||
new JsonField("json")
|
||||
]);
|
||||
|
||||
const test2 = entity("test", {
|
||||
name: text(),
|
||||
checked: boolean({ default_value: false }),
|
||||
count: number(),
|
||||
created_at: date(),
|
||||
updated_at: datetime(),
|
||||
description: text(),
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
});
|
||||
|
||||
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||
});
|
||||
|
||||
test("relations", async () => {
|
||||
const posts = entity("posts", {});
|
||||
const users = entity("users", {});
|
||||
const comments = entity("comments", {});
|
||||
const categories = entity("categories", {});
|
||||
const settings = entity("settings", {});
|
||||
const _media = entity("media", {});
|
||||
|
||||
const relations = [
|
||||
new ManyToOneRelation(posts, users, { mappedBy: "author", required: true }),
|
||||
new OneToOneRelation(users, settings),
|
||||
new ManyToManyRelation(posts, categories),
|
||||
new ManyToOneRelation(comments, users, { required: true }),
|
||||
new ManyToOneRelation(comments, posts, { required: true }),
|
||||
|
||||
// category has single image
|
||||
new PolymorphicRelation(categories, _media, {
|
||||
mappedBy: "image",
|
||||
targetCardinality: 1
|
||||
}),
|
||||
|
||||
// post has multiple images
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 })
|
||||
];
|
||||
|
||||
const relations2 = [
|
||||
relation(posts).manyToOne(users, { mappedBy: "author", required: true }),
|
||||
relation(users).oneToOne(settings),
|
||||
relation(posts).manyToMany(categories),
|
||||
|
||||
relation(comments).manyToOne(users, { required: true }),
|
||||
relation(comments).manyToOne(posts, { required: true }),
|
||||
|
||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||
|
||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" })
|
||||
];
|
||||
|
||||
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||
});
|
||||
|
||||
test("many to many fields", async () => {
|
||||
const posts = entity("posts", {});
|
||||
const categories = entity("categories", {});
|
||||
|
||||
const rel = new ManyToManyRelation(
|
||||
posts,
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
},
|
||||
[new TextField("description")]
|
||||
);
|
||||
|
||||
const fields = {
|
||||
description: text()
|
||||
};
|
||||
let o: FieldSchema<typeof fields>;
|
||||
const rel2 = relation(posts).manyToMany(
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
},
|
||||
fields
|
||||
);
|
||||
|
||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||
});
|
||||
|
||||
test("devexample", async () => {
|
||||
const users = entity("users", {
|
||||
username: text()
|
||||
});
|
||||
|
||||
const comments = entity("comments", {
|
||||
content: text()
|
||||
});
|
||||
|
||||
const posts = entity("posts", {
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
});
|
||||
|
||||
const categories = entity("categories", {
|
||||
name: text(),
|
||||
description: text(),
|
||||
image: medium()
|
||||
});
|
||||
|
||||
const settings = entity("settings", {
|
||||
theme: text()
|
||||
});
|
||||
|
||||
const test = entity("test", {
|
||||
name: text(),
|
||||
checked: boolean({ default_value: false }),
|
||||
count: number(),
|
||||
created_at: date(),
|
||||
updated_at: datetime(),
|
||||
description: text(),
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
});
|
||||
|
||||
const _media = entity("media", {});
|
||||
|
||||
const relations = [
|
||||
relation(posts).manyToOne(users, { mappedBy: "author", required: true }),
|
||||
relation(posts).manyToMany(categories),
|
||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" }),
|
||||
|
||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||
|
||||
relation(users).oneToOne(settings),
|
||||
|
||||
relation(comments).manyToOne(users, { required: true }),
|
||||
relation(comments).manyToOne(posts, { required: true })
|
||||
];
|
||||
|
||||
const obj: Schema<typeof test> = {} as any;
|
||||
});
|
||||
});
|
||||
368
app/__test__/data/relations.test.ts
Normal file
368
app/__test__/data/relations.test.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField
|
||||
} from "../../src/data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("Relations", async () => {
|
||||
test("RelationField", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const schema = em.connection.kysely.schema;
|
||||
|
||||
//const r1 = new RelationField(new Entity("users"));
|
||||
const r1 = new RelationField("users_id", {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
});
|
||||
|
||||
const sql1 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r1.schema()!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql1).toBe(
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)'
|
||||
);
|
||||
|
||||
//const r2 = new RelationField(new Entity("users"), "author");
|
||||
const r2 = new RelationField("author_id", {
|
||||
reference: "author",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
});
|
||||
|
||||
const sql2 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r2.schema()!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql2).toBe(
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)'
|
||||
);
|
||||
});
|
||||
|
||||
test("Required RelationField", async () => {
|
||||
//const r1 = new RelationField(new Entity("users"), undefined, { required: true });
|
||||
const r1 = new RelationField("users_id", {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id",
|
||||
required: true
|
||||
});
|
||||
expect(r1.isRequired()).toBeTrue();
|
||||
});
|
||||
|
||||
test("ManyToOne", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title", {
|
||||
maxLength: 2
|
||||
})
|
||||
]);
|
||||
|
||||
const entities = [users, posts];
|
||||
|
||||
const relationName = "author";
|
||||
const relations = [new ManyToOneRelation(posts, users, { mappedBy: relationName })];
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
expect(rel.source.entity.name).toBe(posts.name);
|
||||
expect(rel.source.reference).toBe(posts.name);
|
||||
expect(rel.target.entity.name).toBe(users.name);
|
||||
expect(rel.target.reference).toBe(relationName);
|
||||
|
||||
// verify field
|
||||
expect(posts.field(relationName + "_id")).toBeInstanceOf(RelationField);
|
||||
|
||||
// verify low level relation
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userPostsRel = em.relationOf(users.name, "posts");
|
||||
expect(userPostsRel).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(userPostsRel?.other(users).entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from posts)
|
||||
const postAuthorRel = em.relationOf(posts.name, "author")! as ManyToOneRelation;
|
||||
expect(postAuthorRel).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(postAuthorRel?.other(posts).entity).toBe(users);
|
||||
|
||||
const kysely = em.connection.kysely;
|
||||
const jsonFrom = (e) => e;
|
||||
/**
|
||||
* Relation Helper
|
||||
*/
|
||||
/**
|
||||
* FROM POSTS
|
||||
* ----------
|
||||
- lhs: posts.author_id
|
||||
- rhs: users.id
|
||||
- as: author
|
||||
- select: users.*
|
||||
- cardinality: 1
|
||||
*/
|
||||
const selectPostsFromUsers = postAuthorRel.buildWith(
|
||||
users,
|
||||
kysely.selectFrom(users.name),
|
||||
jsonFrom,
|
||||
"posts"
|
||||
);
|
||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as "posts" from "users"'
|
||||
);
|
||||
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||
const userObj = { id: 1, username: "test" };
|
||||
expect(postAuthorRel.hydrate(users, [userObj], em)).toEqual(userObj);
|
||||
|
||||
/**
|
||||
FROM USERS
|
||||
----------
|
||||
- lhs: posts.author_id
|
||||
- rhs: users.id
|
||||
- as: posts
|
||||
- select: posts.*
|
||||
- cardinality:
|
||||
*/
|
||||
const selectUsersFromPosts = postAuthorRel.buildWith(
|
||||
posts,
|
||||
kysely.selectFrom(posts.name),
|
||||
jsonFrom,
|
||||
"author"
|
||||
);
|
||||
|
||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
||||
'select (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"'
|
||||
);
|
||||
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||
const postObj = { id: 1, title: "test" };
|
||||
expect(postAuthorRel.hydrate(posts, [postObj], em)).toEqual([postObj]);
|
||||
|
||||
// mutation info
|
||||
expect(postAuthorRel!.helper(users.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
$create: false,
|
||||
$attach: false,
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "n:1"
|
||||
});
|
||||
|
||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "author",
|
||||
local_field: "author_id",
|
||||
$set: true,
|
||||
$create: false,
|
||||
$attach: false,
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "n:1"
|
||||
});
|
||||
|
||||
/*console.log("ManyToOne (source=posts, target=users)");
|
||||
// prettier-ignore
|
||||
console.log("users perspective",postAuthorRel!.helper(users.name)!.getMutationInfo());
|
||||
// prettier-ignore
|
||||
console.log("posts perspective", postAuthorRel!.helper(posts.name)!.getMutationInfo());
|
||||
console.log("");*/
|
||||
});
|
||||
|
||||
test("OneToOne", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const settings = new Entity("settings", [new TextField("theme")]);
|
||||
|
||||
const entities = [users, settings];
|
||||
const relations = [new OneToOneRelation(users, settings)];
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
expect(rel.source.entity.name).toBe(users.name);
|
||||
expect(rel.source.reference).toBe(users.name);
|
||||
expect(rel.target.entity.name).toBe(settings.name);
|
||||
expect(rel.target.reference).toBe(settings.name);
|
||||
|
||||
// verify fields (only one added to users (source))
|
||||
expect(users.field("settings_id")).toBeInstanceOf(RelationField);
|
||||
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||
expect(userSettingRel).toBeInstanceOf(OneToOneRelation);
|
||||
expect(userSettingRel?.other(users).entity.name).toBe(settings.name);
|
||||
|
||||
// verify high level relation (from settings)
|
||||
const settingUserRel = em.relationOf(settings.name, users.name);
|
||||
expect(settingUserRel).toBeInstanceOf(OneToOneRelation);
|
||||
expect(settingUserRel?.other(settings).entity.name).toBe(users.name);
|
||||
|
||||
// mutation info
|
||||
expect(userSettingRel!.helper(users.name)!.getMutationInfo()).toEqual({
|
||||
reference: "settings",
|
||||
local_field: "settings_id",
|
||||
$set: true,
|
||||
$create: true,
|
||||
$attach: false,
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
});
|
||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||
reference: "users",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
$create: false,
|
||||
$attach: false,
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
console.log("OneToOne (source=users, target=settings)");
|
||||
// prettier-ignore
|
||||
console.log("users perspective",userSettingRel!.helper(users.name)!.getMutationInfo());
|
||||
// prettier-ignore
|
||||
console.log("settings perspective", userSettingRel!.helper(settings.name)!.getMutationInfo());
|
||||
console.log("");*/
|
||||
});
|
||||
|
||||
test("ManyToMany", async () => {
|
||||
const posts = new Entity("posts", [new TextField("title")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
|
||||
const entities = [posts, categories];
|
||||
const relations = [new ManyToManyRelation(posts, categories)];
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
//console.log((await em.schema().sync(true)).map((s) => s.sql).join(";\n"));
|
||||
|
||||
// don't expect new fields bc of connection table
|
||||
expect(posts.getFields().length).toBe(2);
|
||||
expect(categories.getFields().length).toBe(2);
|
||||
|
||||
// expect relations set
|
||||
expect(em.relationsOf(posts.name).length).toBe(1);
|
||||
expect(em.relationsOf(categories.name).length).toBe(1);
|
||||
|
||||
// expect connection table with fields
|
||||
expect(em.entity("posts_categories")).toBeInstanceOf(Entity);
|
||||
expect(em.entity("posts_categories").getFields().length).toBe(3);
|
||||
expect(em.entity("posts_categories").field("posts_id")).toBeInstanceOf(RelationField);
|
||||
expect(em.entity("posts_categories").field("categories_id")).toBeInstanceOf(RelationField);
|
||||
|
||||
// verify high level relation (from posts)
|
||||
const postCategoriesRel = em.relationOf(posts.name, categories.name);
|
||||
expect(postCategoriesRel).toBeInstanceOf(ManyToManyRelation);
|
||||
expect(postCategoriesRel?.other(posts).entity.name).toBe(categories.name);
|
||||
|
||||
//console.log("relation", postCategoriesRel);
|
||||
|
||||
// verify high level relation (from posts)
|
||||
const categoryPostsRel = em.relationOf(categories.name, posts.name);
|
||||
expect(categoryPostsRel).toBeInstanceOf(ManyToManyRelation);
|
||||
expect(categoryPostsRel?.other(categories.name).entity.name).toBe(posts.name);
|
||||
|
||||
// now get connection table from relation (from posts)
|
||||
if (postCategoriesRel instanceof ManyToManyRelation) {
|
||||
expect(postCategoriesRel.connectionEntity.name).toBe("posts_categories");
|
||||
expect(em.entity(postCategoriesRel.connectionEntity.name).name).toBe("posts_categories");
|
||||
} else {
|
||||
throw new Error("Expected ManyToManyRelation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Relation Helper
|
||||
*/
|
||||
const kysely = em.connection.kysely;
|
||||
const jsonFrom = (e) => e;
|
||||
|
||||
/**
|
||||
* FROM POSTS
|
||||
* ----------
|
||||
- lhs: posts.author_id
|
||||
- rhs: users.id
|
||||
- as: author
|
||||
- select: users.*
|
||||
- cardinality: 1
|
||||
*/
|
||||
const selectCategoriesFromPosts = postCategoriesRel.buildWith(
|
||||
posts,
|
||||
kysely.selectFrom(posts.name),
|
||||
jsonFrom
|
||||
);
|
||||
expect(selectCategoriesFromPosts.compile().sql).toBe(
|
||||
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"'
|
||||
);
|
||||
|
||||
const selectPostsFromCategories = postCategoriesRel.buildWith(
|
||||
categories,
|
||||
kysely.selectFrom(categories.name),
|
||||
jsonFrom
|
||||
);
|
||||
expect(selectPostsFromCategories.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"'
|
||||
);
|
||||
|
||||
// mutation info
|
||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "categories",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
$create: false,
|
||||
$attach: true,
|
||||
$detach: true,
|
||||
primary: "id",
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
});
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
$create: false,
|
||||
$attach: false,
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
console.log("ManyToMany (source=posts, target=categories)");
|
||||
// prettier-ignore
|
||||
console.log("posts perspective",relations[0].helper(posts.name)!.getMutationInfo());
|
||||
// prettier-ignore
|
||||
console.log("categories perspective", relations[0]!.helper(categories.name)!.getMutationInfo());
|
||||
console.log("");*/
|
||||
});
|
||||
});
|
||||
60
app/__test__/data/specs/Entity.spec.ts
Normal file
60
app/__test__/data/specs/Entity.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, NumberField, TextField } from "../../../src/data";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("description"),
|
||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" })
|
||||
]);
|
||||
|
||||
test("getSelect", async () => {
|
||||
expect(entity.getSelect()).toEqual(["id", "name", "description", "age"]);
|
||||
});
|
||||
|
||||
test("getFillableFields", async () => {
|
||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||
"name",
|
||||
"description",
|
||||
"hidden"
|
||||
]);
|
||||
});
|
||||
|
||||
test("getRequiredFields", async () => {
|
||||
expect(entity.getRequiredFields().map((f) => f.name)).toEqual(["name"]);
|
||||
});
|
||||
|
||||
test("getDefaultObject", async () => {
|
||||
expect(entity.getDefaultObject()).toEqual({
|
||||
age: 18,
|
||||
hidden: "secret"
|
||||
});
|
||||
});
|
||||
|
||||
test("getField", async () => {
|
||||
expect(entity.getField("name")).toBeInstanceOf(TextField);
|
||||
expect(entity.getField("age")).toBeInstanceOf(NumberField);
|
||||
});
|
||||
|
||||
test("getPrimaryField", async () => {
|
||||
expect(entity.getPrimaryField().name).toEqual("id");
|
||||
});
|
||||
|
||||
test("addField", async () => {
|
||||
const field = new TextField("new_field");
|
||||
entity.addField(field);
|
||||
expect(entity.getField("new_field")).toBe(field);
|
||||
});
|
||||
|
||||
// @todo: move this to ClientApp
|
||||
/*test("serialize and deserialize", async () => {
|
||||
const json = entity.toJSON();
|
||||
//sconsole.log("json", json.fields);
|
||||
const newEntity = Entity.deserialize(json);
|
||||
//console.log("newEntity", newEntity.toJSON().fields);
|
||||
expect(newEntity).toBeInstanceOf(Entity);
|
||||
expect(json).toEqual(newEntity.toJSON());
|
||||
expect(json.fields).toEqual(newEntity.toJSON().fields);
|
||||
});*/
|
||||
});
|
||||
106
app/__test__/data/specs/EntityManager.spec.ts
Normal file
106
app/__test__/data/specs/EntityManager.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("[data] EntityManager", async () => {
|
||||
test("base empty throw", async () => {
|
||||
// @ts-expect-error - testing invalid input, connection is required
|
||||
expect(() => new EntityManager([], {})).toThrow(UnableToConnectException);
|
||||
});
|
||||
|
||||
test("base w/o entities & relations", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
expect(em.entities).toEqual([]);
|
||||
expect(em.relations.all).toEqual([]);
|
||||
expect(await em.ping()).toBe(true);
|
||||
expect(() => em.entity("...")).toThrow();
|
||||
expect(() =>
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
||||
).toThrow();
|
||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||
|
||||
// the rest will all throw, since they depend on em.entity()
|
||||
});
|
||||
|
||||
test("w/ 2 entities but no initial relations", async () => {
|
||||
const users = new Entity("users");
|
||||
const posts = new Entity("posts");
|
||||
|
||||
const em = new EntityManager([users, posts], dummyConnection);
|
||||
expect(em.entities).toEqual([users, posts]);
|
||||
expect(em.relations.all).toEqual([]);
|
||||
|
||||
expect(em.entity("users")).toBe(users);
|
||||
expect(em.entity("posts")).toBe(posts);
|
||||
|
||||
// expect adding relation to pass
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
expect(em.relations.all.length).toBe(1);
|
||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]]);
|
||||
expect(em.hasRelations("users")).toBe(true);
|
||||
expect(em.hasRelations("posts")).toBe(true);
|
||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
||||
expect(em.relatedEntitiesOf("posts")).toEqual([users]);
|
||||
expect(em.relationReferencesOf("users")).toEqual(["posts"]);
|
||||
expect(em.relationReferencesOf("posts")).toEqual(["users"]);
|
||||
});
|
||||
|
||||
test("test target relations", async () => {
|
||||
const users = new Entity("users");
|
||||
const posts = new Entity("posts");
|
||||
const comments = new Entity("comments");
|
||||
const categories = new Entity("categories");
|
||||
|
||||
const em = new EntityManager([users, posts, comments, categories], dummyConnection);
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
em.addRelation(new ManyToOneRelation(comments, users));
|
||||
em.addRelation(new ManyToOneRelation(comments, posts));
|
||||
em.addRelation(new ManyToManyRelation(posts, categories));
|
||||
|
||||
const userTargetRel = em.relations.targetRelationsOf(users);
|
||||
const postTargetRel = em.relations.targetRelationsOf(posts);
|
||||
const commentTargetRel = em.relations.targetRelationsOf(comments);
|
||||
|
||||
expect(userTargetRel.map((r) => r.source.entity.name)).toEqual(["posts", "comments"]);
|
||||
expect(postTargetRel.map((r) => r.source.entity.name)).toEqual(["comments"]);
|
||||
expect(commentTargetRel.map((r) => r.source.entity.name)).toEqual([]);
|
||||
});
|
||||
|
||||
test("test listable relations", async () => {
|
||||
const users = new Entity("users");
|
||||
const posts = new Entity("posts");
|
||||
const comments = new Entity("comments");
|
||||
const categories = new Entity("categories");
|
||||
|
||||
const em = new EntityManager([users, posts, comments, categories], dummyConnection);
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
em.addRelation(new ManyToOneRelation(comments, users));
|
||||
em.addRelation(new ManyToOneRelation(comments, posts));
|
||||
em.addRelation(new ManyToManyRelation(posts, categories));
|
||||
|
||||
const userTargetRel = em.relations.listableRelationsOf(users);
|
||||
const postTargetRel = em.relations.listableRelationsOf(posts);
|
||||
const commentTargetRel = em.relations.listableRelationsOf(comments);
|
||||
const categoriesTargetRel = em.relations.listableRelationsOf(categories);
|
||||
|
||||
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||
"comments",
|
||||
"categories"
|
||||
]);
|
||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||
});
|
||||
});
|
||||
43
app/__test__/data/specs/JoinBuilder.spec.ts
Normal file
43
app/__test__/data/specs/JoinBuilder.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("[data] JoinBuilder", async () => {
|
||||
test("missing relation", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
).toThrow('Relation "posts" not found');
|
||||
});
|
||||
|
||||
test("addClause: ManyToOne", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const relations = [new ManyToOneRelation(posts, users, { mappedBy: "author" })];
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
]);
|
||||
|
||||
const res = qb.compile();
|
||||
console.log("compiled", res.sql);
|
||||
|
||||
/*expect(res.sql).toBe(
|
||||
'select from "users" inner join "posts" on "posts"."author_id" = "users"."id" group by "users"."id"',
|
||||
);*/
|
||||
|
||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||
"author"
|
||||
]);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
console.log("compiled2", res2.sql);
|
||||
});
|
||||
});
|
||||
302
app/__test__/data/specs/Mutator.spec.ts
Normal file
302
app/__test__/data/specs/Mutator.spec.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
MutatorEvents,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
type RelationField,
|
||||
RelationMutator,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
const entity = new Entity("items", [
|
||||
new TextField("label", { required: true }),
|
||||
new NumberField("count"),
|
||||
new TextField("hidden", { hidden: true }),
|
||||
new TextField("not_fillable", { fillable: false })
|
||||
]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = { label: "item 1", count: 1 };
|
||||
|
||||
test("insertOne", async () => {
|
||||
expect(em.mutator(entity).getValidatedData(payload, "create")).resolves.toEqual(payload);
|
||||
const res = await em.mutator(entity).insertOne(payload);
|
||||
|
||||
// checking params, because we can't know the id
|
||||
// if it wouldn't be successful, it would throw an error
|
||||
expect(res.parameters).toEqual(Object.values(payload));
|
||||
|
||||
// but expect additional fields to be present
|
||||
expect((res.data as any).not_fillable).toBeDefined();
|
||||
});
|
||||
|
||||
test("updateOne", async () => {
|
||||
const { data } = await em.mutator(entity).insertOne(payload);
|
||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||
count: 2
|
||||
});
|
||||
|
||||
expect(updated.parameters).toEqual([2, data.id]);
|
||||
expect(updated.data.count).toBe(2);
|
||||
});
|
||||
|
||||
test("deleteOne", async () => {
|
||||
const { data } = await em.mutator(entity).insertOne(payload);
|
||||
const deleted = await em.mutator(entity).deleteOne(data.id);
|
||||
|
||||
expect(deleted.parameters).toEqual([data.id]);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("RelationMutator", async () => {
|
||||
// create entries
|
||||
const userData = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const postData = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const postRelMutator = new RelationMutator(posts, em);
|
||||
const postRelField = posts.getField("users_id")! as RelationField;
|
||||
expect(postRelMutator.getRelationalKeys()).toEqual(["users", "users_id"]);
|
||||
|
||||
// persisting relational field should just return key value to be added
|
||||
expect(
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
|
||||
// persisting invalid value should throw
|
||||
expect(postRelMutator.persistRelationField(postRelField, "users_id", 0)).rejects.toThrow();
|
||||
|
||||
// persisting reference should ...
|
||||
expect(
|
||||
postRelMutator.persistReference(relations[0], "users", {
|
||||
$set: { id: userData.data.id }
|
||||
})
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||
|
||||
process.exit(0);
|
||||
|
||||
const userRelMutator = new RelationMutator(users, em);
|
||||
expect(userRelMutator.getRelationalKeys()).toEqual(["posts"]);
|
||||
});
|
||||
|
||||
test("insertOne: missing ref", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: 1 // user does not exist yet
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: missing required relation", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const cats = new Entity("cats");
|
||||
const relations = [new ManyToOneRelation(items, cats, { required: true })];
|
||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||
|
||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
||||
'Field "cats_id" is required'
|
||||
);
|
||||
});
|
||||
|
||||
test("insertOne: using field name", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: data.id
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: null
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
|
||||
test("insertOne: using reference", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: data.id } }
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: null } }
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
|
||||
test("insertOne: performing unsupported operations", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "test",
|
||||
users: { $create: { username: "test" } }
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("updateOne", async () => {
|
||||
const res1 = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res1_1 = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1.data.id } }
|
||||
});
|
||||
expect(up1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1_1.data.id } }
|
||||
});
|
||||
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1.data.id
|
||||
});
|
||||
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1_1.data.id
|
||||
});
|
||||
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: null
|
||||
});
|
||||
expect(up4.data.users_id).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("insertOne: missing ref", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using reference", async () => {
|
||||
// $set is not allowed in OneToOne
|
||||
const { data } = await em.mutator(settings).insertOne({ theme: "dark" });
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $set: { id: data.id } }
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using $create", async () => {
|
||||
const res = await em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $create: { theme: "dark" } }
|
||||
});
|
||||
expect(res.data.settings_id).toBeDefined();
|
||||
});
|
||||
});
|
||||
/*
|
||||
describe("[data] Mutator (ManyToMany)", async () => {
|
||||
const posts = new Entity("posts", [new TextField("title")]);
|
||||
const tags = new Entity("tags", [new TextField("name")]);
|
||||
const relations = [new ManyToOneRelation(posts, tags)];
|
||||
const em = new EntityManager([posts, tags], dummyConnection, relations);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("insertOne: missing ref", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
tags_id: 1, // tag does not exist yet
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using reference", async () => {
|
||||
const { data } = await em.mutator(tags).insertOne({ name: "tag1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
tags: { $attach: { id: data.id } },
|
||||
});
|
||||
expect(res.data.tags).toContain(data.id);
|
||||
});
|
||||
|
||||
test("insertOne: using $create", async () => {
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
tags: { $create: { name: "tag1" } },
|
||||
});
|
||||
expect(res.data.tags).toBeDefined();
|
||||
});
|
||||
|
||||
test("insertOne: using $detach", async () => {
|
||||
const { data: tagData } = await em.mutator(tags).insertOne({ name: "tag1" });
|
||||
const { data: postData } = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
tags: { $attach: { id: tagData.id } },
|
||||
});
|
||||
expect(res.data.tags).toContain(tagData.id);
|
||||
|
||||
const res2 = await em.mutator(posts).updateOne(postData.id, {
|
||||
tags: { $detach: { id: tagData.id } },
|
||||
});
|
||||
expect(res2.data.tags).not.toContain(tagData.id);
|
||||
});
|
||||
});*/
|
||||
|
||||
describe("[data] Mutator (Events)", async () => {
|
||||
const entity = new Entity("test", [new TextField("label")]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
|
||||
const mutator = em.mutator(entity);
|
||||
mutator.emgr.onAny((event) => {
|
||||
// @ts-ignore
|
||||
events.set(event.constructor.slug, event);
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
const { data } = await mutator.insertOne({ label: "test" });
|
||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.updateOne(data.id, { label: "test2" });
|
||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.deleteOne(data.id);
|
||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||
});
|
||||
});
|
||||
222
app/__test__/data/specs/Repository.spec.ts
Normal file
222
app/__test__/data/specs/Repository.spec.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
// @ts-ignore
|
||||
import { Perf } from "@bknd/core/utils";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
describe("[Repository]", async () => {
|
||||
test("bulk", async () => {
|
||||
//const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
const credentials = null as any; // @todo: determine what to do here
|
||||
const connection = new LibsqlConnection(credentials);
|
||||
|
||||
const em = new EntityManager([], connection);
|
||||
/*const emLibsql = new EntityManager([], {
|
||||
url: connection.url.replace("https", "libsql"),
|
||||
authToken: connection.authToken,
|
||||
});*/
|
||||
const table = "posts";
|
||||
|
||||
const client = connection.getClient();
|
||||
if (!client) {
|
||||
console.log("Cannot perform test without libsql connection");
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = em.connection.kysely;
|
||||
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
||||
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
||||
|
||||
async function executeTransaction(em: EntityManager<any>) {
|
||||
return await em.connection.kysely.transaction().execute(async (e) => {
|
||||
const res = await selectQ(e).execute();
|
||||
const count = await countQ(e).execute();
|
||||
|
||||
return [res, count];
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBatch(em: EntityManager<any>) {
|
||||
const queries = [selectQ(conn), countQ(conn)];
|
||||
return await em.connection.batchQuery(queries);
|
||||
}
|
||||
|
||||
async function executeSingleKysely(em: EntityManager<any>) {
|
||||
const res = await selectQ(conn).execute();
|
||||
const count = await countQ(conn).execute();
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
async function executeSingleClient(em: EntityManager<any>) {
|
||||
const q1 = selectQ(conn).compile();
|
||||
const res = await client.execute({
|
||||
sql: q1.sql,
|
||||
args: q1.parameters as any
|
||||
});
|
||||
|
||||
const q2 = countQ(conn).compile();
|
||||
const count = await client.execute({
|
||||
sql: q2.sql,
|
||||
args: q2.parameters as any
|
||||
});
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
const transaction = await executeTransaction(em);
|
||||
const batch = await executeBatch(em);
|
||||
|
||||
expect(batch).toEqual(transaction as any);
|
||||
|
||||
const testperf = false;
|
||||
if (testperf) {
|
||||
const times = 5;
|
||||
|
||||
const exec = async (
|
||||
name: string,
|
||||
fn: (em: EntityManager<any>) => Promise<any>,
|
||||
em: EntityManager<any>
|
||||
) => {
|
||||
const res = await Perf.execute(() => fn(em), times);
|
||||
await sleep(1000);
|
||||
const info = {
|
||||
name,
|
||||
total: res.total.toFixed(2),
|
||||
avg: (res.total / times).toFixed(2),
|
||||
first: res.marks[0].time.toFixed(2),
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2)
|
||||
};
|
||||
console.log(info.name, info, res.marks);
|
||||
return info;
|
||||
};
|
||||
|
||||
const data: any[] = [];
|
||||
data.push(await exec("transaction.http", executeTransaction, em));
|
||||
data.push(await exec("bulk.http", executeBatch, em));
|
||||
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
||||
data.push(await exec("singleCl.http", executeSingleClient, em));
|
||||
|
||||
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
||||
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
||||
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
||||
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
||||
|
||||
console.table(data);
|
||||
/**
|
||||
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
||||
* │ │ name │ total │ avg │ first │ last │
|
||||
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
||||
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
||||
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
||||
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
||||
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
||||
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
||||
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
||||
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
||||
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
||||
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
test("count & exists", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const em = new EntityManager([items], dummyConnection);
|
||||
|
||||
await em.connection.kysely.schema
|
||||
.createTable("items")
|
||||
.ifNotExists()
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("label", "text")
|
||||
.execute();
|
||||
|
||||
// fill
|
||||
await em.connection.kysely
|
||||
.insertInto("items")
|
||||
.values([{ label: "a" }, { label: "b" }, { label: "c" }])
|
||||
.execute();
|
||||
|
||||
// count all
|
||||
const res = await em.repository(items).count();
|
||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||
expect(res.count).toBe(3);
|
||||
|
||||
// count filtered
|
||||
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
||||
|
||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||
expect(res2.parameters).toEqual(["a", "b"]);
|
||||
expect(res2.count).toBe(2);
|
||||
|
||||
// check exists
|
||||
const res3 = await em.repository(items).exists({ label: "a" });
|
||||
expect(res3.exists).toBe(true);
|
||||
|
||||
const res4 = await em.repository(items).exists({ label: "d" });
|
||||
expect(res4.exists).toBe(false);
|
||||
|
||||
// for now, allow empty filter
|
||||
const res5 = await em.repository(items).exists({});
|
||||
expect(res5.exists).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("[data] Repository (Events)", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
const em = new EntityManager([items, categories], dummyConnection, [
|
||||
new ManyToOneRelation(categories, items)
|
||||
]);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
|
||||
em.repository(items).emgr.onAny((event) => {
|
||||
// @ts-ignore
|
||||
events.set(event.constructor.slug, event);
|
||||
});
|
||||
em.repository(categories).emgr.onAny((event) => {
|
||||
// @ts-ignore
|
||||
events.set(event.constructor.slug, event);
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await em.repository(items).findId(1);
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findOne({ id: 1 });
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findMany({ where: { id: 1 } });
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findManyByReference(1, "categories");
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
});
|
||||
});
|
||||
269
app/__test__/data/specs/SchemaManager.spec.ts
Normal file
269
app/__test__/data/specs/SchemaManager.spec.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("SchemaManager tests", async () => {
|
||||
test("introspect entity", async () => {
|
||||
const email = new TextField("email");
|
||||
const entity = new Entity("test", [new TextField("username"), email, new TextField("bio")]);
|
||||
const index = new EntityIndex(entity, [email]);
|
||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||
const schema = new SchemaManager(em);
|
||||
|
||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
||||
expect(introspection).toEqual({
|
||||
name: "test",
|
||||
isView: false,
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
dataType: "TEXT",
|
||||
isNullable: true,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
dataType: "TEXT",
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
dataType: "TEXT",
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
},
|
||||
{
|
||||
name: "bio",
|
||||
dataType: "TEXT",
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
}
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
name: "idx_test_email",
|
||||
table: "test",
|
||||
isUnique: false,
|
||||
columns: [
|
||||
{
|
||||
name: "email",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test("add column", async () => {
|
||||
const table = "add_column";
|
||||
const index = "idx_add_column";
|
||||
const em = new EntityManager(
|
||||
[
|
||||
new Entity(table, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
])
|
||||
],
|
||||
dummyConnection
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
await kysely.schema
|
||||
.createTable(table)
|
||||
.ifNotExists()
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("username", "text")
|
||||
.addColumn("email", "text")
|
||||
.execute();
|
||||
await kysely.schema.createIndex(index).on(table).columns(["username"]).execute();
|
||||
|
||||
const schema = new SchemaManager(em);
|
||||
const diff = await schema.getDiff();
|
||||
|
||||
expect(diff).toEqual([
|
||||
{
|
||||
name: table,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [index] }
|
||||
}
|
||||
]);
|
||||
|
||||
// now sync
|
||||
await schema.sync({ force: true, drop: true });
|
||||
const diffAfter = await schema.getDiff();
|
||||
|
||||
console.log("diffAfter", diffAfter);
|
||||
expect(diffAfter.length).toBe(0);
|
||||
|
||||
await kysely.schema.dropTable(table).execute();
|
||||
});
|
||||
|
||||
test("drop column", async () => {
|
||||
const table = "drop_column";
|
||||
const em = new EntityManager(
|
||||
[new Entity(table, [new TextField("username")])],
|
||||
dummyConnection
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
await kysely.schema
|
||||
.createTable(table)
|
||||
.ifNotExists()
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("username", "text")
|
||||
.addColumn("email", "text")
|
||||
.execute();
|
||||
|
||||
const schema = new SchemaManager(em);
|
||||
const diff = await schema.getDiff();
|
||||
|
||||
expect(diff).toEqual([
|
||||
{
|
||||
name: table,
|
||||
isNew: false,
|
||||
columns: {
|
||||
add: [],
|
||||
drop: ["email"],
|
||||
change: []
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// now sync
|
||||
await schema.sync({ force: true, drop: true });
|
||||
const diffAfter = await schema.getDiff();
|
||||
|
||||
//console.log("diffAfter", diffAfter);
|
||||
expect(diffAfter.length).toBe(0);
|
||||
|
||||
await kysely.schema.dropTable(table).execute();
|
||||
});
|
||||
|
||||
test("create table and add column", async () => {
|
||||
const usersTable = "create_users";
|
||||
const postsTable = "create_posts";
|
||||
const em = new EntityManager(
|
||||
[
|
||||
new Entity(usersTable, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
]),
|
||||
new Entity(postsTable, [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at")
|
||||
])
|
||||
],
|
||||
dummyConnection
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
await kysely.schema
|
||||
.createTable(usersTable)
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("username", "text")
|
||||
.addColumn("email", "text")
|
||||
.execute();
|
||||
|
||||
const schema = new SchemaManager(em);
|
||||
const diff = await schema.getDiff();
|
||||
|
||||
expect(diff).toEqual([
|
||||
{
|
||||
name: usersTable,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [] }
|
||||
},
|
||||
{
|
||||
name: postsTable,
|
||||
isNew: true,
|
||||
columns: {
|
||||
add: ["id", "title", "content", "created_at"],
|
||||
drop: [],
|
||||
change: []
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// now sync
|
||||
await schema.sync({ force: true });
|
||||
const diffAfter = await schema.getDiff();
|
||||
|
||||
//console.log("diffAfter", diffAfter);
|
||||
expect(diffAfter.length).toBe(0);
|
||||
|
||||
await kysely.schema.dropTable(usersTable).execute();
|
||||
await kysely.schema.dropTable(postsTable).execute();
|
||||
});
|
||||
|
||||
test("adds index on create", async () => {
|
||||
const entity = new Entity(randomString(16), [new TextField("email")]);
|
||||
const index = new EntityIndex(entity, [entity.getField("email")!]);
|
||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||
|
||||
const diff = await em.schema().getDiff();
|
||||
expect(diff).toEqual([
|
||||
{
|
||||
name: entity.name,
|
||||
isNew: true,
|
||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const diffAfter = await em.schema().getDiff();
|
||||
expect(diffAfter.length).toBe(0);
|
||||
});
|
||||
|
||||
test("adds index after", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const entity = new Entity(randomString(16), [new TextField("email", { required: true })]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
// now add index
|
||||
const index = new EntityIndex(entity, [entity.getField("email")!], true);
|
||||
em.addIndex(index);
|
||||
|
||||
const diff = await em.schema().getDiff();
|
||||
expect(diff).toEqual([
|
||||
{
|
||||
name: entity.name,
|
||||
isNew: false,
|
||||
columns: { add: [], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const diffAfter = await em.schema().getDiff();
|
||||
expect(diffAfter.length).toBe(0);
|
||||
});
|
||||
});
|
||||
195
app/__test__/data/specs/WithBuilder.spec.ts
Normal file
195
app/__test__/data/specs/WithBuilder.spec.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder
|
||||
} from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("[data] WithBuilder", async () => {
|
||||
test("missing relation", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
).toThrow('Relation "posts" not found');
|
||||
});
|
||||
|
||||
test("addClause: ManyToOne", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const relations = [new ManyToOneRelation(posts, users, { mappedBy: "author" })];
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
]);
|
||||
|
||||
const res = qb.compile();
|
||||
|
||||
expect(res.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" where "users"."id" = "posts"."author_id" limit ?) as agg) as "posts" from "users"'
|
||||
);
|
||||
expect(res.parameters).toEqual([5]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
posts, // @todo: try with "users", it gives output!
|
||||
["author"]
|
||||
);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" where "posts"."author_id" = "users"."id" limit ?) as obj) as "author" from "posts"'
|
||||
);
|
||||
expect(res2.parameters).toEqual([1]);
|
||||
});
|
||||
|
||||
test("test with empty join", async () => {
|
||||
const qb = { qb: 1 } as any;
|
||||
|
||||
expect(WithBuilder.addClause(null as any, qb, null as any, [])).toBe(qb);
|
||||
});
|
||||
|
||||
test("test manytomany", async () => {
|
||||
const posts = new Entity("posts", [new TextField("title")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
|
||||
const entities = [posts, categories];
|
||||
const relations = [new ManyToManyRelation(posts, categories)];
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
await em.mutator(posts).insertOne({ title: "fashion post" });
|
||||
await em.mutator(posts).insertOne({ title: "beauty post" });
|
||||
|
||||
await em.mutator(categories).insertOne({ label: "fashion" });
|
||||
await em.mutator(categories).insertOne({ label: "beauty" });
|
||||
await em.mutator(categories).insertOne({ label: "tech" });
|
||||
|
||||
await em.connection.kysely
|
||||
.insertInto("posts_categories")
|
||||
.values([
|
||||
{ posts_id: 1, categories_id: 1 },
|
||||
{ posts_id: 2, categories_id: 2 },
|
||||
{ posts_id: 1, categories_id: 2 }
|
||||
])
|
||||
.execute();
|
||||
|
||||
//console.log((await em.repository().findMany("posts_categories")).result);
|
||||
|
||||
const res = await em.repository(posts).findMany({ with: ["categories"] });
|
||||
|
||||
expect(res.data).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
title: "fashion post",
|
||||
categories: [
|
||||
{ id: 1, label: "fashion" },
|
||||
{ id: 2, label: "beauty" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "beauty post",
|
||||
categories: [{ id: 2, label: "beauty" }]
|
||||
}
|
||||
]);
|
||||
|
||||
const res2 = await em.repository(categories).findMany({ with: ["posts"] });
|
||||
|
||||
//console.log(res2.sql, res2.data);
|
||||
|
||||
expect(res2.data).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
label: "fashion",
|
||||
posts: [{ id: 1, title: "fashion post" }]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "beauty",
|
||||
posts: [
|
||||
{ id: 2, title: "beauty post" },
|
||||
{ id: 1, title: "fashion post" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "tech",
|
||||
posts: []
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("polymorphic", async () => {
|
||||
const categories = new Entity("categories", [new TextField("name")]);
|
||||
const media = new Entity("media", [new TextField("path")]);
|
||||
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
const em = new EntityManager(entities, dummyConnection, [single, multiple]);
|
||||
|
||||
const qb = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
["single"]
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as obj) as "single" from "categories"'
|
||||
);
|
||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
["multiple"]
|
||||
);
|
||||
const res2 = qb2.compile();
|
||||
expect(res2.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as agg) as "multiple" from "categories"'
|
||||
);
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 5]);
|
||||
});
|
||||
|
||||
/*test("test manytoone", async () => {
|
||||
const posts = new Entity("posts", [new TextField("title")]);
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const relations = [
|
||||
new ManyToOneRelation(posts, users, { mappedBy: "author" }),
|
||||
];
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
console.log((await em.schema().sync(true)).map((s) => s.sql).join("\n"));
|
||||
await em.schema().sync();
|
||||
|
||||
await em.mutator().insertOne("users", { username: "user1" });
|
||||
await em.mutator().insertOne("users", { username: "user2" });
|
||||
|
||||
await em.mutator().insertOne("posts", { title: "post1", author_id: 1 });
|
||||
await em.mutator().insertOne("posts", { title: "post2", author_id: 2 });
|
||||
|
||||
console.log((await em.repository().findMany("posts")).result);
|
||||
|
||||
const res = await em.repository().findMany("posts", { join: ["author"] });
|
||||
console.log(res.sql, res.parameters, res.result);
|
||||
});*/
|
||||
});
|
||||
92
app/__test__/data/specs/connection/Connection.spec.ts
Normal file
92
app/__test__/data/specs/connection/Connection.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
describe("Connection", async () => {
|
||||
test("it introspects indices correctly", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
await kysely.schema.createTable("items").ifNotExists().addColumn("name", "text").execute();
|
||||
await kysely.schema.createIndex("idx_items_name").on("items").columns(["name"]).execute();
|
||||
|
||||
const indices = await em.connection.getIntrospector().getIndices("items");
|
||||
expect(indices).toEqual([
|
||||
{
|
||||
name: "idx_items_name",
|
||||
table: "items",
|
||||
isUnique: false,
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("it introspects indices on multiple columns correctly", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
await kysely.schema
|
||||
.createTable("items_multiple")
|
||||
.ifNotExists()
|
||||
.addColumn("name", "text")
|
||||
.addColumn("desc", "text")
|
||||
.execute();
|
||||
await kysely.schema
|
||||
.createIndex("idx_items_multiple")
|
||||
.on("items_multiple")
|
||||
.columns(["name", "desc"])
|
||||
.execute();
|
||||
|
||||
const indices = await em.connection.getIntrospector().getIndices("items_multiple");
|
||||
expect(indices).toEqual([
|
||||
{
|
||||
name: "idx_items_multiple",
|
||||
table: "items_multiple",
|
||||
isUnique: false,
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
order: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("it introspects unique indices correctly", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const kysely = em.connection.kysely;
|
||||
const tbl_name = "items_unique";
|
||||
const idx_name = "idx_items_unique";
|
||||
|
||||
await kysely.schema.createTable(tbl_name).ifNotExists().addColumn("name", "text").execute();
|
||||
await kysely.schema.createIndex(idx_name).on(tbl_name).columns(["name"]).unique().execute();
|
||||
|
||||
const indices = await em.connection.getIntrospector().getIndices(tbl_name);
|
||||
expect(indices).toEqual([
|
||||
{
|
||||
name: idx_name,
|
||||
table: tbl_name,
|
||||
isUnique: true,
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
29
app/__test__/data/specs/fields/BooleanField.spec.ts
Normal file
29
app/__test__/data/specs/fields/BooleanField.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BooleanField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
expect(field.transformRetrieve(1)).toBe(true);
|
||||
expect(field.transformRetrieve(0)).toBe(false);
|
||||
expect(field.transformRetrieve("1")).toBe(true);
|
||||
expect(field.transformRetrieve("0")).toBe(false);
|
||||
expect(field.transformRetrieve(true)).toBe(true);
|
||||
expect(field.transformRetrieve(false)).toBe(false);
|
||||
expect(field.transformRetrieve(null)).toBe(null);
|
||||
expect(field.transformRetrieve(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
test("transformPersist (specific)", async () => {
|
||||
const field = new BooleanField("test");
|
||||
expect(transformPersist(field, 1)).resolves.toBe(true);
|
||||
expect(transformPersist(field, 0)).resolves.toBe(false);
|
||||
expect(transformPersist(field, "1")).rejects.toThrow();
|
||||
expect(transformPersist(field, "0")).rejects.toThrow();
|
||||
expect(transformPersist(field, true)).resolves.toBe(true);
|
||||
expect(transformPersist(field, false)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
13
app/__test__/data/specs/fields/DateField.spec.ts
Normal file
13
app/__test__/data/specs/fields/DateField.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
|
||||
describe("[data] DateField", async () => {
|
||||
runBaseFieldTests(DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||
|
||||
// @todo: add datefield tests
|
||||
test("week", async () => {
|
||||
const field = new DateField("test", { type: "week" });
|
||||
console.log(field.getValue("2021-W01", "submit"));
|
||||
});
|
||||
});
|
||||
44
app/__test__/data/specs/fields/EnumField.spec.ts
Normal file
44
app/__test__/data/specs/fields/EnumField.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EnumField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
function options(strings: string[]) {
|
||||
return { type: "strings", values: strings };
|
||||
}
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
runBaseFieldTests(
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
{ options: options(["a", "b", "c"]) }
|
||||
);
|
||||
|
||||
test("yields if no options", async () => {
|
||||
expect(() => new EnumField("test", { options: options([]) })).toThrow();
|
||||
});
|
||||
|
||||
test("yields if default value is not a valid option", async () => {
|
||||
expect(
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("transformPersist (config)", async () => {
|
||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||
|
||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||
expect(transformPersist(field, "a")).resolves.toBe("a");
|
||||
expect(transformPersist(field, "d")).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new EnumField("test", {
|
||||
options: options(["a", "b", "c"]),
|
||||
default_value: "a",
|
||||
required: true
|
||||
});
|
||||
|
||||
expect(field.transformRetrieve(null)).toBe("a");
|
||||
expect(field.transformRetrieve("d")).toBe("a");
|
||||
});
|
||||
});
|
||||
45
app/__test__/data/specs/fields/Field.spec.ts
Normal file
45
app/__test__/data/specs/fields/Field.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
schema(): SchemaResponse {
|
||||
return this.useSchemaHelper("text");
|
||||
}
|
||||
getSchema() {
|
||||
return baseFieldConfigSchema;
|
||||
}
|
||||
}
|
||||
|
||||
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
|
||||
test.only("default config", async () => {
|
||||
const field = new FieldSpec("test");
|
||||
const config = Default(baseFieldConfigSchema, {});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
||||
console.log("config", new TextField("test", { required: true }).toJSON());
|
||||
});
|
||||
|
||||
test("transformPersist (specific)", async () => {
|
||||
const required = new FieldSpec("test", { required: true });
|
||||
const requiredDefault = new FieldSpec("test", {
|
||||
required: true,
|
||||
default_value: "test"
|
||||
});
|
||||
|
||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||
expect(
|
||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
||||
).rejects.toThrow();
|
||||
|
||||
// works because it has a default value
|
||||
expect(
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
||||
).resolves.toBeDefined();
|
||||
expect(
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
38
app/__test__/data/specs/fields/FieldIndex.spec.ts
Normal file
38
app/__test__/data/specs/fields/FieldIndex.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityIndex,
|
||||
type EntityManager,
|
||||
Field,
|
||||
type SchemaResponse
|
||||
} from "../../../../src/data";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
return Type.Any();
|
||||
}
|
||||
|
||||
schema(em: EntityManager<any>): SchemaResponse {
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
describe("FieldIndex", async () => {
|
||||
const entity = new Entity("test", []);
|
||||
test("it constructs", async () => {
|
||||
const field = new TestField("name");
|
||||
const index = new EntityIndex(entity, [field]);
|
||||
|
||||
expect(index.fields).toEqual([field]);
|
||||
expect(index.name).toEqual("idx_test_name");
|
||||
expect(index.unique).toEqual(false);
|
||||
});
|
||||
|
||||
test("it fails on non-unique", async () => {
|
||||
const field = new TestField("name", { required: false });
|
||||
|
||||
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
||||
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
||||
});
|
||||
});
|
||||
47
app/__test__/data/specs/fields/JsonField.spec.ts
Normal file
47
app/__test__/data/specs/fields/JsonField.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
runBaseFieldTests(JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text"
|
||||
});
|
||||
|
||||
test("transformPersist (no config)", async () => {
|
||||
expect(transformPersist(field, Function)).rejects.toThrow();
|
||||
expect(transformPersist(field, undefined)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("isSerializable", async () => {
|
||||
expect(field.isSerializable(1)).toBe(true);
|
||||
expect(field.isSerializable("test")).toBe(true);
|
||||
expect(field.isSerializable({ test: 1 })).toBe(true);
|
||||
expect(field.isSerializable({ test: [1, 2] })).toBe(true);
|
||||
expect(field.isSerializable(Function)).toBe(false);
|
||||
expect(field.isSerializable(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test("isSerialized", async () => {
|
||||
expect(field.isSerialized(1)).toBe(false);
|
||||
expect(field.isSerialized({ test: 1 })).toBe(false);
|
||||
expect(field.isSerialized('{"test":1}')).toBe(true);
|
||||
expect(field.isSerialized("1")).toBe(true);
|
||||
});
|
||||
|
||||
test("getValue", async () => {
|
||||
expect(field.getValue({ test: 1 }, "form")).toBe('{"test":1}');
|
||||
expect(field.getValue("string", "form")).toBe('"string"');
|
||||
expect(field.getValue(1, "form")).toBe("1");
|
||||
|
||||
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
||||
expect(field.getValue('"string"', "submit")).toBe("string");
|
||||
expect(field.getValue("1", "submit")).toBe(1);
|
||||
|
||||
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
||||
expect(field.getValue("string", "table")).toBe('"string"');
|
||||
expect(field.getValue(1, "form")).toBe("1");
|
||||
});
|
||||
});
|
||||
9
app/__test__/data/specs/fields/JsonSchemaField.spec.ts
Normal file
9
app/__test__/data/specs/fields/JsonSchemaField.spec.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonSchemaField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||
|
||||
// @todo: add JsonSchemaField tests
|
||||
});
|
||||
19
app/__test__/data/specs/fields/NumberField.spec.ts
Normal file
19
app/__test__/data/specs/fields/NumberField.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { NumberField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
const field = new NumberField("test", { minimum: 3, maximum: 5 });
|
||||
|
||||
expect(transformPersist(field, 2)).rejects.toThrow();
|
||||
expect(transformPersist(field, 6)).rejects.toThrow();
|
||||
expect(transformPersist(field, 4)).resolves.toBe(4);
|
||||
|
||||
const field2 = new NumberField("test");
|
||||
expect(transformPersist(field2, 0)).resolves.toBe(0);
|
||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||
});
|
||||
|
||||
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
});
|
||||
37
app/__test__/data/specs/fields/PrimaryField.spec.ts
Normal file
37
app/__test__/data/specs/fields/PrimaryField.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
|
||||
test("name", async () => {
|
||||
expect(field.name).toBe("primary");
|
||||
});
|
||||
|
||||
test("schema", () => {
|
||||
expect(field.name).toBe("primary");
|
||||
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]);
|
||||
});
|
||||
|
||||
test("hasDefault", async () => {
|
||||
expect(field.hasDefault()).toBe(false);
|
||||
expect(field.getDefault()).toBe(undefined);
|
||||
});
|
||||
|
||||
test("isFillable", async () => {
|
||||
expect(field.isFillable()).toBe(false);
|
||||
});
|
||||
|
||||
test("isHidden", async () => {
|
||||
expect(field.isHidden()).toBe(false);
|
||||
});
|
||||
|
||||
test("isRequired", async () => {
|
||||
expect(field.isRequired()).toBe(false);
|
||||
});
|
||||
|
||||
test("transformPersist/Retrieve", async () => {
|
||||
expect(field.transformPersist(1)).rejects.toThrow();
|
||||
expect(field.transformRetrieve(1)).toBe(1);
|
||||
});
|
||||
});
|
||||
15
app/__test__/data/specs/fields/TextField.spec.ts
Normal file
15
app/__test__/data/specs/fields/TextField.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TextField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
|
||||
describe("[data] TextField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
const field = new TextField("test", { minLength: 3, maxLength: 5 });
|
||||
|
||||
expect(transformPersist(field, "a")).rejects.toThrow();
|
||||
expect(transformPersist(field, "abcdefghijklmn")).rejects.toThrow();
|
||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||
});
|
||||
|
||||
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
});
|
||||
162
app/__test__/data/specs/fields/inc.ts
Normal file
162
app/__test__/data/specs/fields/inc.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { ColumnDataType } from "kysely";
|
||||
import { omit } from "lodash-es";
|
||||
import type { BaseFieldConfig, Field, TActionContext } from "../../../../src/data";
|
||||
|
||||
type ConstructableField = new (name: string, config?: Partial<BaseFieldConfig>) => Field;
|
||||
|
||||
type FieldTestConfig = {
|
||||
defaultValue: any;
|
||||
sampleValues?: any[];
|
||||
schemaType: ColumnDataType;
|
||||
};
|
||||
|
||||
export function transformPersist(field: Field, value: any, context?: TActionContext) {
|
||||
return field.transformPersist(value, undefined as any, context as any);
|
||||
}
|
||||
|
||||
export function runBaseFieldTests(
|
||||
fieldClass: ConstructableField,
|
||||
config: FieldTestConfig,
|
||||
_requiredConfig: any = {}
|
||||
) {
|
||||
const noConfigField = new fieldClass("no_config", _requiredConfig);
|
||||
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
|
||||
const required = new fieldClass("required", { ..._requiredConfig, required: true });
|
||||
const hidden = new fieldClass("hidden", { ..._requiredConfig, hidden: true });
|
||||
const dflt = new fieldClass("dflt", { ..._requiredConfig, default_value: config.defaultValue });
|
||||
const requiredAndDefault = new fieldClass("full", {
|
||||
..._requiredConfig,
|
||||
fillable: true,
|
||||
required: true,
|
||||
default_value: config.defaultValue
|
||||
});
|
||||
|
||||
test("schema", () => {
|
||||
expect(noConfigField.name).toBe("no_config");
|
||||
expect(noConfigField.schema(null as any)).toEqual([
|
||||
"no_config",
|
||||
config.schemaType,
|
||||
expect.any(Function)
|
||||
]);
|
||||
});
|
||||
|
||||
test("hasDefault", async () => {
|
||||
expect(noConfigField.hasDefault()).toBe(false);
|
||||
expect(noConfigField.getDefault()).toBeUndefined();
|
||||
expect(dflt.hasDefault()).toBe(true);
|
||||
expect(dflt.getDefault()).toBe(config.defaultValue);
|
||||
});
|
||||
|
||||
test("isFillable", async () => {
|
||||
expect(noConfigField.isFillable()).toBe(true);
|
||||
expect(fillable.isFillable()).toBe(true);
|
||||
expect(hidden.isFillable()).toBe(true);
|
||||
expect(required.isFillable()).toBe(true);
|
||||
});
|
||||
|
||||
test("isHidden", async () => {
|
||||
expect(noConfigField.isHidden()).toBe(false);
|
||||
expect(hidden.isHidden()).toBe(true);
|
||||
expect(fillable.isHidden()).toBe(false);
|
||||
expect(required.isHidden()).toBe(false);
|
||||
});
|
||||
|
||||
test("isRequired", async () => {
|
||||
expect(noConfigField.isRequired()).toBe(false);
|
||||
expect(required.isRequired()).toBe(true);
|
||||
expect(hidden.isRequired()).toBe(false);
|
||||
expect(fillable.isRequired()).toBe(false);
|
||||
});
|
||||
|
||||
test.if(Array.isArray(config.sampleValues))("getValue (RenderContext)", async () => {
|
||||
const isPrimitive = (v) => ["string", "number"].includes(typeof v);
|
||||
for (const value of config.sampleValues!) {
|
||||
// "form"
|
||||
expect(isPrimitive(noConfigField.getValue(value, "form"))).toBeTrue();
|
||||
// "table"
|
||||
expect(isPrimitive(noConfigField.getValue(value, "table"))).toBeTrue();
|
||||
// "read"
|
||||
// "submit"
|
||||
}
|
||||
});
|
||||
|
||||
test("transformPersist", async () => {
|
||||
const persist = await transformPersist(noConfigField, config.defaultValue);
|
||||
expect(config.defaultValue).toEqual(noConfigField.transformRetrieve(config.defaultValue));
|
||||
expect(transformPersist(noConfigField, null)).resolves.toBeUndefined();
|
||||
expect(transformPersist(noConfigField, undefined)).resolves.toBeUndefined();
|
||||
expect(transformPersist(requiredAndDefault, null)).resolves.toBe(persist);
|
||||
expect(transformPersist(dflt, null)).resolves.toBe(persist);
|
||||
});
|
||||
|
||||
test("toJSON", async () => {
|
||||
const _config = {
|
||||
..._requiredConfig,
|
||||
//order: 1,
|
||||
fillable: true,
|
||||
required: false,
|
||||
hidden: false
|
||||
//virtual: false,
|
||||
//default_value: undefined
|
||||
};
|
||||
|
||||
function fieldJson(field: Field) {
|
||||
const json = field.toJSON();
|
||||
return {
|
||||
...json,
|
||||
config: omit(json.config, ["html"])
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(fieldJson(hidden)).toEqual({
|
||||
//name: "hidden",
|
||||
type: required.type,
|
||||
config: {
|
||||
..._config,
|
||||
hidden: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(fieldJson(dflt)).toEqual({
|
||||
//name: "dflt",
|
||||
type: dflt.type,
|
||||
config: {
|
||||
..._config,
|
||||
default_value: config.defaultValue
|
||||
}
|
||||
});
|
||||
|
||||
expect(fieldJson(requiredAndDefault)).toEqual({
|
||||
//name: "full",
|
||||
type: requiredAndDefault.type,
|
||||
config: {
|
||||
..._config,
|
||||
fillable: true,
|
||||
required: true,
|
||||
default_value: config.defaultValue
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
78
app/__test__/data/specs/relations/EntityRelation.spec.ts
Normal file
78
app/__test__/data/specs/relations/EntityRelation.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
RelationTypes
|
||||
} from "../../../../src/data/relations";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
super(
|
||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||
config
|
||||
);
|
||||
}
|
||||
initialize(em: EntityManager<any>) {}
|
||||
type() {
|
||||
return RelationTypes.ManyToOne; /* doesn't matter */
|
||||
}
|
||||
setDirections(directions: ("source" | "target")[]) {
|
||||
this.directions = directions;
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(a: any, b: any): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
describe("[data] EntityRelation", async () => {
|
||||
test("other", async () => {
|
||||
const relation = new TestEntityRelation();
|
||||
expect(relation.other("source").entity.name).toBe("target");
|
||||
expect(relation.other("target").entity.name).toBe("source");
|
||||
});
|
||||
|
||||
it("visibleFrom", async () => {
|
||||
const relation = new TestEntityRelation();
|
||||
// by default, both sides are visible
|
||||
expect(relation.visibleFrom("source")).toBe(true);
|
||||
expect(relation.visibleFrom("target")).toBe(true);
|
||||
|
||||
// make source invisible
|
||||
relation.setDirections(["target"]);
|
||||
expect(relation.visibleFrom("source")).toBe(false);
|
||||
expect(relation.visibleFrom("target")).toBe(true);
|
||||
|
||||
// make target invisible
|
||||
relation.setDirections(["source"]);
|
||||
expect(relation.visibleFrom("source")).toBe(true);
|
||||
expect(relation.visibleFrom("target")).toBe(false);
|
||||
});
|
||||
|
||||
it("hydrate", async () => {
|
||||
// @todo: implement
|
||||
});
|
||||
|
||||
it("isListableFor", async () => {
|
||||
// by default, the relation is listable from target side
|
||||
const relation = new TestEntityRelation();
|
||||
expect(relation.isListableFor(relation.target.entity)).toBe(true);
|
||||
expect(relation.isListableFor(relation.source.entity)).toBe(false);
|
||||
});
|
||||
|
||||
it("required", async () => {
|
||||
const relation1 = new TestEntityRelation();
|
||||
expect(relation1.config.required).toBe(false);
|
||||
|
||||
const relation2 = new TestEntityRelation({ required: true });
|
||||
expect(relation2.config.required).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user