diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts index ca861d5..9091fbb 100644 --- a/app/__test__/modules/AppAuth.spec.ts +++ b/app/__test__/modules/AppAuth.spec.ts @@ -4,6 +4,7 @@ import { AuthController } from "../../src/auth/api/AuthController"; import { em, entity, make, text } from "../../src/data"; import { AppAuth, type ModuleBuildContext } from "../../src/modules"; import { disableConsoleLog, enableConsoleLog } from "../helper"; +// @ts-ignore import { makeCtx, moduleTestSuite } from "./module-test-suite"; describe("AppAuth", () => { @@ -22,7 +23,7 @@ describe("AppAuth", () => { const config = auth.toJSON(); expect(config.jwt).toBeUndefined(); - expect(config.strategies.password.config).toBeUndefined(); + expect(config.strategies?.password?.config).toBeUndefined(); }); test("enabling auth: generate secret", async () => { @@ -42,6 +43,7 @@ describe("AppAuth", () => { const auth = new AppAuth( { enabled: true, + // @ts-ignore jwt: { secret: "123456", }, @@ -75,7 +77,7 @@ describe("AppAuth", () => { const { data: users } = await ctx.em.repository("users").findMany(); expect(users.length).toBe(1); - expect(users[0].email).toBe("some@body.com"); + expect(users[0]?.email).toBe("some@body.com"); } }); diff --git a/app/__test__/modules/ModuleManager.spec.ts b/app/__test__/modules/ModuleManager.spec.ts index c556795..31b2027 100644 --- a/app/__test__/modules/ModuleManager.spec.ts +++ b/app/__test__/modules/ModuleManager.spec.ts @@ -43,7 +43,7 @@ describe("ModuleManager", async () => { }).toJSON(), }, }, - }); + }) as any; //const { version, ...json } = mm.toJSON() as any; const c2 = getDummyConnection(); @@ -73,7 +73,7 @@ describe("ModuleManager", async () => { }).toJSON(), }, }, - }; + } as any; //const { version, ...json } = mm.toJSON() as any; const { dummyConnection } = getDummyConnection(); @@ -90,23 +90,20 @@ describe("ModuleManager", async () => { await mm2.build(); expect(stripMark(json)).toEqual(stripMark(mm2.configs())); - expect(mm2.configs().data.entities.test).toBeDefined(); - expect(mm2.configs().data.entities.test.fields.content).toBeDefined(); - expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined(); + expect(mm2.configs().data.entities?.test).toBeDefined(); + expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined(); + expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined(); }); test("s4: config given, table exists, version outdated, migrate", async () => { const c = getDummyConnection(); const mm = new ModuleManager(c.dummyConnection); await mm.build(); - const version = mm.version(); const json = mm.configs(); const c2 = getDummyConnection(); const db = c2.dummyConnection.kysely; - const mm2 = new ModuleManager(c2.dummyConnection, { - initial: { version: version - 1, ...json }, - }); + const mm2 = new ModuleManager(c2.dummyConnection); await mm2.syncConfigTable(); await db @@ -171,7 +168,7 @@ describe("ModuleManager", async () => { expect(mm2.configs().data.basepath).toBe("/api/data2"); }); - test("blank app, modify config", async () => { + /*test("blank app, modify config", async () => { const { dummyConnection } = getDummyConnection(); const mm = new ModuleManager(dummyConnection); @@ -194,7 +191,7 @@ describe("ModuleManager", async () => { }, }, }); - }); + });*/ test("partial config given", async () => { const { dummyConnection } = getDummyConnection(); @@ -212,13 +209,15 @@ describe("ModuleManager", async () => { expect(mm.version()).toBe(CURRENT_VERSION); expect(mm.built()).toBe(true); expect(mm.configs().auth.enabled).toBe(true); - expect(mm.configs().data.entities.users).toBeDefined(); + expect(mm.configs().data.entities?.users).toBeDefined(); }); test("partial config given, but db version exists", async () => { const c = getDummyConnection(); const mm = new ModuleManager(c.dummyConnection); await mm.build(); + console.log("==".repeat(30)); + console.log(""); const json = mm.configs(); const c2 = getDummyConnection(); @@ -265,8 +264,8 @@ describe("ModuleManager", async () => { override async build() { //console.log("building FailingModule", this.config); - if (this.config.value < 0) { - throw new Error("value must be positive"); + if (this.config.value && this.config.value < 0) { + throw new Error("value must be positive, given: " + this.config.value); } this.setBuilt(); } @@ -332,6 +331,7 @@ describe("ModuleManager", async () => { // @ts-ignore const f = mm.mutateConfigSafe("failing"); + // @ts-ignore expect(f.set({ value: 2 })).resolves.toBeDefined(); expect(mockOnUpdated).toHaveBeenCalled(); }); diff --git a/app/__test__/modules/migrations/migrations.spec.ts b/app/__test__/modules/migrations/migrations.spec.ts index de004ac..4bd83ab 100644 --- a/app/__test__/modules/migrations/migrations.spec.ts +++ b/app/__test__/modules/migrations/migrations.spec.ts @@ -1,39 +1,44 @@ import { describe, expect, test } from "bun:test"; import { type InitialModuleConfigs, createApp } from "../../../src"; -import type { Kysely } from "kysely"; +import { type Kysely, sql } from "kysely"; import { getDummyConnection } from "../../helper"; import v7 from "./samples/v7.json"; +import v8 from "./samples/v8.json"; +import v8_2 from "./samples/v8-2.json"; // app expects migratable config to be present in database -async function createVersionedApp(config: InitialModuleConfigs) { +async function createVersionedApp(config: InitialModuleConfigs | any) { const { dummyConnection } = getDummyConnection(); if (!("version" in config)) throw new Error("config must have a version"); const { version, ...rest } = config; - const app = createApp({ connection: dummyConnection }); - await app.build(); + const db = dummyConnection.kysely as Kysely; + await sql`CREATE TABLE "__bknd" ( + "id" integer not null primary key autoincrement, + "version" integer, + "type" text, + "json" text, + "created_at" datetime, + "updated_at" datetime + )`.execute(db); - const qb = app.modules.ctx().connection.kysely as Kysely; - const current = await qb - .selectFrom("__bknd") - .selectAll() - .where("type", "=", "config") - .executeTakeFirst(); - - await qb - .updateTable("__bknd") - .set("json", JSON.stringify(rest)) - .set("version", 7) - .where("id", "=", current!.id) + await db + .insertInto("__bknd") + .values({ + version, + type: "config", + created_at: new Date().toISOString(), + json: JSON.stringify(rest), + }) .execute(); - const app2 = createApp({ + const app = createApp({ connection: dummyConnection, }); - await app2.build(); - return app2; + await app.build(); + return app; } describe("Migrations", () => { @@ -46,8 +51,8 @@ describe("Migrations", () => { const app = await createVersionedApp(v7); - expect(app.version()).toBe(8); - expect(app.toJSON(true).auth.strategies.password.enabled).toBe(true); + expect(app.version()).toBeGreaterThan(7); + expect(app.toJSON(true).auth.strategies?.password?.enabled).toBe(true); const req = await app.server.request("/api/auth/password/register", { method: "POST", @@ -60,7 +65,17 @@ describe("Migrations", () => { }), }); expect(req.ok).toBe(true); - const res = await req.json(); + const res = (await req.json()) as any; expect(res.user.email).toBe("test@test.com"); }); + + test("migration from 8 to 9", async () => { + expect(v8.version).toBe(8); + + const app = await createVersionedApp(v8); + + expect(app.version()).toBeGreaterThan(8); + // @ts-expect-error + expect(app.toJSON(true).server.admin).toBeUndefined(); + }); }); diff --git a/app/__test__/modules/migrations/samples/v8-2.json b/app/__test__/modules/migrations/samples/v8-2.json new file mode 100644 index 0000000..c94e85a --- /dev/null +++ b/app/__test__/modules/migrations/samples/v8-2.json @@ -0,0 +1,709 @@ +{ + "version": 8, + "server": { + "admin": { + "basepath": "", + "color_scheme": "light", + "logo_return_path": "/" + }, + "cors": { + "origin": "*", + "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"], + "allow_headers": [ + "Content-Type", + "Content-Length", + "Authorization", + "Accept" + ] + } + }, + "data": { + "basepath": "/api/data", + "entities": { + "products": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "title": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "brand": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "currency": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "price": { + "type": "number", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "price_compare": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": ["table"] + } + }, + "url": { + "type": "text", + "config": { + "html_config": { + "element": "input" + }, + "required": true, + "fillable": true, + "hidden": ["table"] + } + }, + "created_at": { + "type": "date", + "config": { + "type": "date", + "required": false, + "fillable": true, + "hidden": ["table"] + } + }, + "description": { + "type": "text", + "config": { + "html_config": { + "element": "textarea", + "props": { + "rows": 4 + } + }, + "required": false, + "fillable": true, + "hidden": ["table"] + } + }, + "images": { + "type": "media", + "config": { + "required": false, + "fillable": ["update"], + "hidden": false, + "mime_types": [], + "virtual": true, + "entity": "products" + } + }, + "identifier": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "metadata": { + "type": "jsonschema", + "config": { + "schema": { + "type": "object", + "properties": { + "size": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "ai_description": { + "type": "string" + } + }, + "additionalProperties": { + "type": ["string", "number"] + } + }, + "required": false, + "fillable": true, + "hidden": ["table"] + } + }, + "_likes": { + "type": "number", + "config": { + "default_value": 0, + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc" + } + }, + "media": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "path": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "folder": { + "type": "boolean", + "config": { + "default_value": false, + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "mime_type": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "size": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "scope": { + "type": "text", + "config": { + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "etag": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "modified_at": { + "type": "date", + "config": { + "type": "datetime", + "required": false, + "fillable": true, + "hidden": false + } + }, + "reference": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "entity_id": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "metadata": { + "type": "json", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "temporary": { + "type": "boolean", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc" + } + }, + "users": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "email": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "strategy": { + "type": "enum", + "config": { + "options": { + "type": "strings", + "values": ["password"] + }, + "required": true, + "fillable": ["create"], + "hidden": ["read", "table", "update", "form"] + } + }, + "strategy_value": { + "type": "text", + "config": { + "fillable": ["create"], + "hidden": ["read", "table", "update", "form"], + "required": true + } + }, + "role": { + "type": "enum", + "config": { + "options": { + "type": "strings", + "values": ["guest", "admin"] + }, + "required": false, + "fillable": true, + "hidden": false + } + }, + "username": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "name": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "_boards": { + "type": "number", + "config": { + "default_value": 0, + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc" + } + }, + "product_likes": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "created_at": { + "type": "date", + "config": { + "type": "datetime", + "required": false, + "fillable": true, + "hidden": false + } + }, + "users_id": { + "type": "relation", + "config": { + "label": "User", + "required": true, + "reference": "users", + "target": "users", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + }, + "products_id": { + "type": "relation", + "config": { + "label": "Product", + "required": true, + "reference": "products", + "target": "products", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc", + "name": "Product Likes" + } + }, + "boards": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "private": { + "type": "boolean", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "description": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "users_id": { + "type": "relation", + "config": { + "label": "Users", + "required": true, + "reference": "users", + "target": "users", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + }, + "images": { + "type": "media", + "config": { + "required": false, + "fillable": ["update"], + "hidden": false, + "mime_types": [], + "virtual": true, + "entity": "boards", + "max_items": 5 + } + }, + "cover": { + "type": "number", + "config": { + "default_value": 0, + "required": false, + "fillable": true, + "hidden": false + } + }, + "_products": { + "type": "number", + "config": { + "default_value": 0, + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc" + } + }, + "boards_products": { + "type": "generated", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "boards_id": { + "type": "relation", + "config": { + "required": true, + "reference": "boards", + "target": "boards", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + }, + "products_id": { + "type": "relation", + "config": { + "required": true, + "reference": "products", + "target": "products", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + }, + "manual": { + "type": "boolean", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "desc" + } + } + }, + "relations": { + "poly_products_media_images": { + "type": "poly", + "source": "products", + "target": "media", + "config": { + "mappedBy": "images" + } + }, + "n1_product_likes_users": { + "type": "n:1", + "source": "product_likes", + "target": "users", + "config": { + "mappedBy": "", + "inversedBy": "", + "required": true, + "with_limit": 5 + } + }, + "n1_product_likes_products": { + "type": "n:1", + "source": "product_likes", + "target": "products", + "config": { + "mappedBy": "", + "inversedBy": "", + "required": true, + "with_limit": 5 + } + }, + "n1_boards_users": { + "type": "n:1", + "source": "boards", + "target": "users", + "config": { + "mappedBy": "", + "inversedBy": "", + "required": true, + "with_limit": 5 + } + }, + "poly_boards_media_images": { + "type": "poly", + "source": "boards", + "target": "media", + "config": { + "mappedBy": "images", + "targetCardinality": 5 + } + }, + "mn_boards_products_boards_products,boards_products": { + "type": "m:n", + "source": "boards", + "target": "products", + "config": {} + } + }, + "indices": { + "idx_unique_media_path": { + "entity": "media", + "fields": ["path"], + "unique": true + }, + "idx_media_reference": { + "entity": "media", + "fields": ["reference"], + "unique": false + }, + "idx_unique_users_email": { + "entity": "users", + "fields": ["email"], + "unique": true + }, + "idx_users_strategy": { + "entity": "users", + "fields": ["strategy"], + "unique": false + }, + "idx_users_strategy_value": { + "entity": "users", + "fields": ["strategy_value"], + "unique": false + }, + "idx_product_likes_unique_products_id_users_id": { + "entity": "product_likes", + "fields": ["products_id", "users_id"], + "unique": true + }, + "idx_boards_products_unique_boards_id_products_id": { + "entity": "boards_products", + "fields": ["boards_id", "products_id"], + "unique": true + }, + "idx_media_entity_id": { + "entity": "media", + "fields": ["entity_id"], + "unique": false + }, + "idx_products_identifier": { + "entity": "products", + "fields": ["identifier"], + "unique": false + } + } + }, + "auth": { + "enabled": true, + "basepath": "/api/auth", + "entity_name": "users", + "allow_register": true, + "jwt": { + "secret": "2wY76Z$JQg(3t?Wn8g,^ZqZhlmjNx<@uQjI!7i^XZBF11Xa1>zZK2??Y[D|]cc%k", + "alg": "HS256", + "fields": ["id", "email", "role"] + }, + "cookie": { + "path": "/", + "sameSite": "lax", + "secure": true, + "httpOnly": true, + "expires": 604800, + "renew": true, + "pathSuccess": "/", + "pathLoggedOut": "/" + }, + "strategies": { + "password": { + "enabled": true, + "type": "password", + "config": { + "hashing": "sha256" + } + } + }, + "roles": { + "guest": { + "is_default": true, + "permissions": ["system.access.api", "data.entity.read"] + }, + "admin": { + "implicit_allow": true + } + }, + "guard": { + "enabled": true + } + }, + "media": { + "enabled": true, + "basepath": "/api/media", + "entity_name": "media", + "storage": {}, + "adapter": { + "type": "s3", + "config": { + "access_key": "123", + "secret_access_key": "123", + "url": "https://123.r2.cloudflarestorage.com/enly" + } + } + }, + "flows": { + "basepath": "/api/flows", + "flows": {} + } +} diff --git a/app/__test__/modules/migrations/samples/v8.json b/app/__test__/modules/migrations/samples/v8.json new file mode 100644 index 0000000..e4bacee --- /dev/null +++ b/app/__test__/modules/migrations/samples/v8.json @@ -0,0 +1,598 @@ +{ + "version": 8, + "server": { + "admin": { + "basepath": "", + "logo_return_path": "/", + "color_scheme": "dark" + }, + "cors": { + "origin": "*", + "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"], + "allow_headers": [ + "Content-Type", + "Content-Length", + "Authorization", + "Accept" + ] + } + }, + "data": { + "basepath": "/api/data", + "entities": { + "posts": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "title": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "slug": { + "type": "text", + "config": { + "html_config": { + "element": "input" + }, + "pattern": "^[a-z\\-\\_0-9]+$", + "required": false, + "fillable": true, + "hidden": false, + "label": "Slug" + } + }, + "content": { + "type": "text", + "config": { + "html_config": { + "element": "textarea", + "props": { + "rows": 5 + } + }, + "required": false, + "fillable": true, + "hidden": ["form"] + } + }, + "active": { + "type": "boolean", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "images": { + "type": "media", + "config": { + "required": false, + "fillable": ["update"], + "hidden": false, + "mime_types": [], + "virtual": true, + "entity": "posts" + } + }, + "tags": { + "type": "jsonschema", + "config": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "ui_schema": { + "ui:options": { + "orderable": false + } + }, + "required": false, + "fillable": true, + "hidden": false + } + }, + "users_id": { + "type": "relation", + "config": { + "label": "Users", + "required": false, + "reference": "users", + "target": "users", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + } + }, + "config": { + "sort_field": "title", + "sort_dir": "desc" + } + }, + "comments": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "content": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "created_at": { + "type": "date", + "config": { + "type": "date", + "required": false, + "fillable": true, + "hidden": false + } + }, + "posts_id": { + "type": "relation", + "config": { + "label": "Posts", + "required": true, + "reference": "posts", + "target": "posts", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + }, + "users_id": { + "type": "relation", + "config": { + "label": "Users", + "required": true, + "reference": "users", + "target": "users", + "target_field": "id", + "fillable": true, + "hidden": false, + "on_delete": "set null" + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "media": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "path": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "folder": { + "type": "boolean", + "config": { + "default_value": false, + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "mime_type": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "size": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "scope": { + "type": "text", + "config": { + "hidden": true, + "fillable": ["create"], + "required": false + } + }, + "etag": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "modified_at": { + "type": "date", + "config": { + "type": "datetime", + "required": false, + "fillable": true, + "hidden": false + } + }, + "reference": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "entity_id": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "metadata": { + "type": "json", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "users": { + "type": "system", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "email": { + "type": "text", + "config": { + "required": true, + "fillable": true, + "hidden": false + } + }, + "strategy": { + "type": "enum", + "config": { + "options": { + "type": "strings", + "values": ["password", "google"] + }, + "required": true, + "fillable": ["create"], + "hidden": ["update", "form"] + } + }, + "strategy_value": { + "type": "text", + "config": { + "fillable": ["create"], + "hidden": ["read", "table", "update", "form"], + "required": true + } + }, + "role": { + "type": "enum", + "config": { + "options": { + "type": "strings", + "values": ["guest", "admin", "editor"] + }, + "required": false, + "fillable": true, + "hidden": false + } + }, + "avatar": { + "type": "media", + "config": { + "required": false, + "fillable": ["update"], + "hidden": false, + "mime_types": [], + "virtual": true, + "entity": "users", + "max_items": 1 + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + }, + "test": { + "type": "regular", + "fields": { + "id": { + "type": "primary", + "config": { + "fillable": false, + "required": false, + "hidden": false + } + }, + "title": { + "type": "text", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + }, + "number": { + "type": "number", + "config": { + "required": false, + "fillable": true, + "hidden": false + } + } + }, + "config": { + "sort_field": "id", + "sort_dir": "asc" + } + } + }, + "relations": { + "n1_comments_posts": { + "type": "n:1", + "source": "comments", + "target": "posts", + "config": { + "required": true, + "with_limit": 5 + } + }, + "poly_posts_media_images": { + "type": "poly", + "source": "posts", + "target": "media", + "config": { + "mappedBy": "images" + } + }, + "n1_posts_users": { + "type": "n:1", + "source": "posts", + "target": "users", + "config": { + "with_limit": 5 + } + }, + "n1_comments_users": { + "type": "n:1", + "source": "comments", + "target": "users", + "config": { + "required": true, + "with_limit": 5 + } + }, + "poly_users_media_avatar": { + "type": "poly", + "source": "users", + "target": "media", + "config": { + "mappedBy": "avatar", + "targetCardinality": 1 + } + } + }, + "indices": { + "idx_unique_media_path": { + "entity": "media", + "fields": ["path"], + "unique": true + }, + "idx_unique_users_email": { + "entity": "users", + "fields": ["email"], + "unique": true + }, + "idx_users_strategy": { + "entity": "users", + "fields": ["strategy"], + "unique": false + }, + "idx_users_strategy_value": { + "entity": "users", + "fields": ["strategy_value"], + "unique": false + }, + "idx_media_reference": { + "entity": "media", + "fields": ["reference"], + "unique": false + }, + "idx_media_entity_id": { + "entity": "media", + "fields": ["entity_id"], + "unique": false + } + } + }, + "auth": { + "enabled": true, + "basepath": "/api/auth", + "entity_name": "users", + "jwt": { + "secret": "A%3jk*wD!Zruj123123123j$Wm8qS8m8qS8", + "alg": "HS256", + "fields": ["id", "email", "role"], + "issuer": "showoff" + }, + "guard": { + "enabled": true + }, + "strategies": { + "password": { + "enabled": true, + "type": "password", + "config": { + "hashing": "sha256" + } + }, + "google": { + "enabled": true, + "type": "oauth", + "config": { + "type": "oidc", + "client": { + "client_id": "545948917277-123ieuifrag.apps.googleusercontent.com", + "client_secret": "123-123hTTZfDDGPDPp" + }, + "name": "google" + } + } + }, + "roles": { + "guest": { + "permissions": [ + "data.entity.read", + "system.access.api", + "system.config.read" + ], + "is_default": true + }, + "admin": { + "is_default": false, + "implicit_allow": true + }, + "editor": { + "permissions": [ + "system.access.admin", + "system.config.read", + "system.schema.read", + "system.config.read.secrets", + "system.access.api", + "data.entity.read", + "data.entity.update", + "data.entity.delete", + "data.entity.create" + ] + } + }, + "allow_register": true, + "cookie": { + "path": "/", + "sameSite": "lax", + "secure": true, + "httpOnly": true, + "expires": 604800, + "renew": true, + "pathSuccess": "/", + "pathLoggedOut": "/" + } + }, + "media": { + "enabled": true, + "basepath": "/api/media", + "entity_name": "media", + "storage": {}, + "adapter": { + "type": "s3", + "config": { + "access_key": "123", + "secret_access_key": "123", + "url": "https://123.r2.cloudflarestorage.com/bknd-123" + } + } + }, + "flows": { + "basepath": "/api/flows", + "flows": { + "test": { + "trigger": { + "type": "http", + "config": { + "mode": "sync", + "method": "GET", + "response_type": "html", + "path": "/json-posts" + } + }, + "tasks": { + "fetching": { + "type": "fetch", + "params": { + "method": "GET", + "headers": [], + "url": "https://jsonplaceholder.typicode.com/posts" + } + }, + "render": { + "type": "render", + "params": { + "render": "

Posts

\n" + } + } + }, + "connections": { + "5cce66b5-57c6-4541-88ac-b298794c6c52": { + "source": "fetching", + "target": "render", + "config": { + "condition": { + "type": "success" + } + } + } + }, + "start_task": "fetching", + "responding_task": "render" + } + } + } +} diff --git a/app/src/core/utils/DebugLogger.ts b/app/src/core/utils/DebugLogger.ts index e58b45d..23a1b44 100644 --- a/app/src/core/utils/DebugLogger.ts +++ b/app/src/core/utils/DebugLogger.ts @@ -30,7 +30,7 @@ export class DebugLogger { const now = performance.now(); const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last)); - const indents = " ".repeat(this._context.length); + const indents = " ".repeat(Math.max(this._context.length - 1, 0)); const context = this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : ""; console.log(indents, context, time, ...args); diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 185ee09..22fad1b 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -19,8 +19,6 @@ export class AppData extends Module { indices: _indices = {}, } = this.config; - this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities)); - const entities = transformObject(_entities, (entityConfig, name) => { return constructEntity(name, entityConfig); }); @@ -60,7 +58,6 @@ export class AppData extends Module { ); this.ctx.guard.registerPermissions(Object.values(DataPermissions)); - this.ctx.logger.clear(); this.setBuilt(); } diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 955b6c4..8f47582 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -115,7 +115,7 @@ const configJsonSchema = Type.Union([ }), ), ]); -const __bknd = entity(TABLE_NAME, { +export const __bknd = entity(TABLE_NAME, { version: number().required(), type: enumm({ enum: ["config", "diff", "backup"] }).required(), json: jsonSchema({ schema: configJsonSchema }).required(), @@ -170,6 +170,8 @@ export class ModuleManager { } } + this.logger.log("booted with", this._booted_with); + this.createModules(initial); } @@ -218,7 +220,8 @@ export class ModuleManager { private repo() { return this.__em.repo(__bknd, { - silent: !debug_modules, + // to prevent exceptions when table doesn't exist + silent: true, }); } @@ -271,7 +274,7 @@ export class ModuleManager { }; } - private async fetch(): Promise { + private async fetch(): Promise { this.logger.context("fetch").log("fetching"); const startTime = performance.now(); @@ -285,7 +288,7 @@ export class ModuleManager { if (!result) { this.logger.log("error fetching").clear(); - throw BkndError.with("no config"); + return undefined; } this.logger @@ -305,6 +308,7 @@ export class ModuleManager { try { const state = await this.fetch(); + if (!state) throw new BkndError("save: no config found"); this.logger.log("fetched version", state.version); if (state.version !== version) { @@ -321,11 +325,11 @@ export class ModuleManager { json: configs, }); } else { - this.logger.log("version matches"); + this.logger.log("version matches", state.version); // clean configs because of Diff() function const diffs = diff(state.json, clone(configs)); - this.logger.log("checking diff", diffs); + this.logger.log("checking diff", [diffs.length]); if (diff.length > 0) { // store diff @@ -380,78 +384,6 @@ export class ModuleManager { return this; } - private async migrate() { - const state = { - success: false, - migrated: false, - version: { - before: this.version(), - after: this.version(), - }, - }; - this.logger.context("migrate").log("migrating?", this.version(), CURRENT_VERSION); - - if (this.version() < CURRENT_VERSION) { - state.version.before = this.version(); - - this.logger.log("there are migrations, verify version"); - // sync __bknd table - await this.syncConfigTable(); - - // modules must be built before migration - this.logger.log("building modules"); - await this.buildModules({ graceful: true }); - this.logger.log("modules built"); - - try { - const state = await this.fetch(); - if (state.version !== this.version()) { - // @todo: potentially drop provided config and use database version - throw new Error( - `Given version (${this.version()}) and fetched version (${state.version}) do not match.`, - ); - } - } catch (e: any) { - throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`); - } - - this.logger.log("now migrating"); - let version = this.version(); - let configs: any = this.configs(); - //console.log("migrating with", version, configs); - if (Object.keys(configs).length === 0) { - throw new Error("No config to migrate"); - } - - const [_version, _configs] = await migrate(version, configs, { - db: this.db, - }); - version = _version; - configs = _configs; - - this._version = version; - state.version.after = version; - state.migrated = true; - this.ctx().flags.sync_required = true; - - this.logger.log("setting configs"); - this.createModules(configs); - await this.buildModules(); - - this.logger.log("migrated to", version); - $console.log("Migrated config from", state.version.before, "to", state.version.after); - - await this.save(); - } else { - this.logger.log("no migrations needed"); - } - - state.success = true; - this.logger.clear(); - - return state; - } - private setConfigs(configs: ModuleConfigs): void { this.logger.log("setting configs"); objectEach(configs, (config, key) => { @@ -469,66 +401,66 @@ export class ModuleManager { async build(opts?: { fetch?: boolean }) { this.logger.context("build").log("version", this.version()); - this.logger.log("booted with", this._booted_with); // if no config provided, try fetch from db if (this.version() === 0 || opts?.fetch === true) { - if (this.version() === 0) { - this.logger.context("no version").log("version is 0"); - } else { - this.logger.context("force fetch").log("force fetch"); + if (opts?.fetch) { + this.logger.log("force fetch"); } - try { - const result = await this.fetch(); + const result = await this.fetch(); + // if no version, and nothing found, go with initial + if (!result) { + this.logger.log("nothing in database, go initial"); + await this.setupInitial(); + } else { + this.logger.log("db has", result.version); // set version and config from fetched this._version = result.version; - if (this.version() !== CURRENT_VERSION) { - await this.syncConfigTable(); - } - if (this.options?.trustFetched === true) { this.logger.log("trusting fetched config (mark)"); mark(result.json); } - this.setConfigs(result.json); - } catch (e: any) { - this.logger.clear(); // fetch couldn't clear + // if version doesn't match, migrate before building + if (this.version() !== CURRENT_VERSION) { + this.logger.log("now migrating"); - this.logger.context("error handler").log("fetch failed", e.message); + await this.syncConfigTable(); - // we can safely build modules, since config version is up to date - // it's up to date because we use default configs (no fetch result) - this._version = CURRENT_VERSION; - await this.syncConfigTable(); - const state = await this.buildModules(); - if (!state.saved) { - await this.save(); + const version_before = this.version(); + const [_version, _configs] = await migrate(version_before, result.json, { + db: this.db, + }); + + this._version = _version; + this.ctx().flags.sync_required = true; + + this.logger.log("migrated to", _version); + $console.log("Migrated config from", version_before, "to", this.version()); + + this.createModules(_configs); + await this.buildModules(); + } else { + this.logger.log("version is current", this.version()); + this.createModules(result.json); + await this.buildModules(); } - - // run initial setup - await this.setupInitial(); - - this.logger.clear(); - return this; } - this.logger.clear(); - } - - // migrate to latest if needed - this.logger.log("check migrate"); - const migration = await this.migrate(); - if (migration.success && migration.migrated) { - this.logger.log("skipping build after migration"); } else { - this.logger.log("trigger build modules"); + if (this.version() !== CURRENT_VERSION) { + throw new Error( + `Given version (${this.version()}) and current version (${CURRENT_VERSION}) do not match.`, + ); + } + this.logger.log("current version is up to date", this.version()); await this.buildModules(); } this.logger.log("done"); + this.logger.clear(); return this; } @@ -589,6 +521,14 @@ export class ModuleManager { } protected async setupInitial() { + this.logger.context("initial").log("start"); + this._version = CURRENT_VERSION; + await this.syncConfigTable(); + const state = await this.buildModules(); + if (!state.saved) { + await this.save(); + } + const ctx = { ...this.ctx(), // disable events for initial setup @@ -601,6 +541,7 @@ export class ModuleManager { // run first boot event await this.options?.onFirstBoot?.(); + this.logger.clear(); } mutateConfigSafe( diff --git a/app/src/modules/migrations.ts b/app/src/modules/migrations.ts index 8297a45..59911b1 100644 --- a/app/src/modules/migrations.ts +++ b/app/src/modules/migrations.ts @@ -1,6 +1,7 @@ import { _jsonp, transformObject } from "core/utils"; import { type Kysely, sql } from "kysely"; import { set } from "lodash-es"; +import type { InitialModuleConfigs } from "modules/ModuleManager"; export type MigrationContext = { db: Kysely; @@ -91,6 +92,17 @@ export const migrations: Migration[] = [ }; }, }, + { + // remove admin config + version: 9, + up: async (config) => { + const { admin, ...server } = config.server; + return { + ...config, + server, + }; + }, + }, ]; export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0; diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index 5957467..f6a665a 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -5,9 +5,9 @@ import { config, isDebug } from "core"; import { addFlashMessage } from "core/server/flash"; import { html } from "hono/html"; import { Fragment } from "hono/jsx"; +import { css, Style } from "hono/css"; import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; -import type { AppTheme } from "modules/server/AppServer"; const htmlBkndContextReplace = ""; @@ -73,7 +73,6 @@ export class AdminController extends Controller { const obj = { user: c.get("auth")?.user, logout_route: this.withBasePath(authRoutes.logout), - color_scheme: configs.server.admin.color_scheme, }; const html = await this.getHtml(obj); if (!html) { @@ -183,14 +182,13 @@ export class AdminController extends Controller { assets.css = manifest["src/ui/main.tsx"].css[0] as any; } - const theme = configs.server.admin.color_scheme ?? "light"; const favicon = isProd ? this.options.assets_path + "favicon.ico" : "/favicon.ico"; return ( {/* dnd complains otherwise */} {html``} - +
-
- - Initializing... - +