merge: stack widget modernization on dnd kit

This commit is contained in:
Thomas Camlong committed 2026-08-08 20:40:26 +02:00
commit 9edeba5cd2
940 files changed
+104475 -33649

No files matched your search

+7 -4
View File
@@ -1,4 +1,4 @@
/* eslint-disable import/namespace -- Schema tables are selected by validated typed keys. */
/* eslint-disable import/namespace -- Schema tables are intentionally selected by typed runtime keys. */
import type { InferInsertModel } from "drizzle-orm";
import { objectEntries } from "@homarr/common";
@@ -36,8 +36,10 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
db.transaction((transaction) => {
for (const [key, values] of objectEntries(context)) {
if (values.length >= 1) {
// oxlint-disable-next-line import/namespace -- TableKey limits key to exported schema tables.
const table = schema[key];
transaction
.insert(schema[key])
.insert(table)
.values(values as never)
.run();
}
@@ -51,14 +53,15 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
for (const [key, values] of objectEntries(context)) {
if (values.length >= 1) {
// Below is actually the mysqlSchema when the driver is mysql
await transaction.insert(schema[key] as never).values(values as never);
// oxlint-disable-next-line import/namespace -- TableKey limits key to exported schema tables.
const table = schema[key] as never;
await transaction.insert(table).values(values as never);
}
}
});
},
};
};
export const createDbInsertCollectionWithoutTransaction = <TTableKey extends TableKey>(
tablesInInsertOrder: TTableKey[],
) => {
@@ -0,0 +1,168 @@
/** @vitest-environment node */
import { parse, stringify } from "superjson";
import { describe, expect, test } from "vitest";
import { createId } from "@homarr/common";
import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db";
import { createDb } from "@homarr/db/test";
import { containerSectionOptionsSchema } from "@homarr/validation/shared";
import {
boards,
itemLayouts,
items,
layouts,
sectionCollapseStates,
sectionLayouts,
sections,
users,
} from "../../schema";
import { migrateLegacySectionsToContainersAsync } from "./0004_unify_sections_and_gutters";
describe("legacy section migration", () => {
test("converts a rail category into a resizable container inside a board gutter", async () => {
const db = createDb();
const userId = createId();
const boardId = createId();
const layoutId = createId();
const mainRootId = createId();
const categoryId = createId();
const itemId = createId();
await db.insert(users).values({ id: userId });
await db.insert(boards).values({ id: boardId, name: "migration", creatorId: userId });
await db.insert(layouts).values({ id: layoutId, name: "Base", boardId, columnCount: 8, breakpoint: 0 });
await db.insert(sections).values([
{ id: mainRootId, boardId, kind: "empty", xOffset: 0, yOffset: 0 },
{
id: categoryId,
boardId,
kind: "category" as never,
name: "Pinned apps",
xOffset: 0,
yOffset: 1,
options: stringify({
showLabel: false,
collapsible: true,
showOpenAll: true,
railPlacement: "left",
columnCount: 2,
}),
},
]);
await db.insert(items).values({ id: itemId, boardId, kind: "clock" });
await db.insert(sectionCollapseStates).values({
sectionId: categoryId,
userId,
// Legacy categories stored true when the section was open.
collapsed: true,
});
await db.insert(itemLayouts).values({
itemId,
layoutId,
sectionId: categoryId,
xOffset: 1,
yOffset: 0,
width: 1,
height: 1,
});
await migrateLegacySectionsToContainersAsync(db as unknown as Database);
const migratedLayout = await db.query.layouts.findFirst({ where: eq(layouts.id, layoutId) });
expect(migratedLayout?.leftGutterColumnCount).toBe(2);
const gutterRoot = await db.query.sections.findFirst({
where: and(eq(sections.boardId, boardId), eq(sections.kind, "empty"), eq(sections.xOffset, -1)),
});
expect(gutterRoot).toBeDefined();
const migratedCategory = await db.query.sections.findFirst({ where: eq(sections.id, categoryId) });
expect(migratedCategory?.kind).toBe("container");
expect(containerSectionOptionsSchema.parse(parse(migratedCategory?.options ?? "{}"))).toEqual({
title: "Pinned apps",
customCssClasses: [],
borderColor: "",
showLabel: false,
collapsible: true,
showOpenAll: true,
});
const migratedCollapseState = await db.query.sectionCollapseStates.findFirst({
where: and(eq(sectionCollapseStates.sectionId, categoryId), eq(sectionCollapseStates.userId, userId)),
});
expect(migratedCollapseState?.collapsed).toBe(false);
const categoryLayout = await db.query.sectionLayouts.findFirst({
where: and(eq(sectionLayouts.sectionId, categoryId), eq(sectionLayouts.layoutId, layoutId)),
});
expect(categoryLayout).toMatchObject({
parentSectionId: gutterRoot?.id,
width: 2,
height: 1,
});
const nestedItem = await db.query.itemLayouts.findFirst({
where: and(eq(itemLayouts.itemId, itemId), eq(itemLayouts.layoutId, layoutId)),
});
expect(nestedItem?.sectionId).toBe(categoryId);
});
test("renames a dynamic section and preserves all container options and geometry", async () => {
const db = createDb();
const userId = createId();
const boardId = createId();
const layoutId = createId();
const rootId = createId();
const sectionId = createId();
const options = {
title: "Operations",
customCssClasses: ["dense", "accent"],
borderColor: "#123456",
showLabel: false,
collapsible: true,
showOpenAll: true,
};
await db.insert(users).values({ id: userId });
await db.insert(boards).values({ id: boardId, name: "dynamic-migration", creatorId: userId });
await db.insert(layouts).values({ id: layoutId, name: "Base", boardId, columnCount: 8, breakpoint: 0 });
await db.insert(sections).values([
{ id: rootId, boardId, kind: "empty", xOffset: 0, yOffset: 0 },
{
id: sectionId,
boardId,
kind: "dynamic" as never,
options: stringify(options),
},
]);
await db.insert(sectionLayouts).values({
sectionId,
layoutId,
parentSectionId: rootId,
xOffset: 2,
yOffset: 3,
width: 4,
height: 5,
});
await migrateLegacySectionsToContainersAsync(db as unknown as Database);
await migrateLegacySectionsToContainersAsync(db as unknown as Database);
const migrated = await db.query.sections.findFirst({ where: eq(sections.id, sectionId) });
expect(migrated?.kind).toBe("container");
expect(containerSectionOptionsSchema.parse(parse(migrated?.options ?? "{}"))).toEqual(options);
expect(
await db.query.sectionLayouts.findFirst({
where: and(eq(sectionLayouts.sectionId, sectionId), eq(sectionLayouts.layoutId, layoutId)),
}),
).toMatchObject({
parentSectionId: rootId,
xOffset: 2,
yOffset: 3,
width: 4,
height: 5,
});
});
});
@@ -0,0 +1,541 @@
import { parse, stringify } from "superjson";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import type { BoardLane } from "@homarr/definitions";
import { emptySuperJSON, getRootSectionLane, rootSectionOffsets } from "@homarr/definitions";
import { containerSectionOptionsSchema } from "@homarr/validation/shared";
import { and, eq, handleTransactionsAsync, inArray } from "../..";
import type { Database } from "../..";
import type { HomarrDatabase } from "../../driver";
import { itemLayouts, layouts, sectionCollapseStates, sectionLayouts, sections } from "../../schema";
interface MigrationPlacement {
id: string;
type: "item" | "section";
x: number;
y: number;
w: number;
h: number;
}
interface MigrationBoard {
items: {
id: string;
layouts: {
layoutId: string;
sectionId: string;
xOffset: number;
yOffset: number;
width: number;
height: number;
}[];
}[];
sections: {
id: string;
kind: string;
layouts: {
layoutId: string;
parentSectionId: string | null;
xOffset: number;
yOffset: number;
width: number;
height: number;
}[];
}[];
}
interface MigrationOperation {
applyAsync(db: Database): Promise<void>;
applySync(db: HomarrDatabase): void;
}
const legacyCategoryOptionsDefaults = {
showLabel: true,
collapsible: true,
showOpenAll: true,
railPlacement: "main",
columnCount: 2,
} as const;
const legacyCategoryOptionsSchema = z
.object({
showLabel: z.boolean().default(legacyCategoryOptionsDefaults.showLabel),
collapsible: z.boolean().default(legacyCategoryOptionsDefaults.collapsible),
showOpenAll: z.boolean().default(legacyCategoryOptionsDefaults.showOpenAll),
railPlacement: z.enum(["main", "left", "right"]).default(legacyCategoryOptionsDefaults.railPlacement),
columnCount: z.number().int().min(1).max(24).default(legacyCategoryOptionsDefaults.columnCount),
})
.default(legacyCategoryOptionsDefaults);
/**
* Convert legacy categories and dynamic sections into durable containers.
* Categories were full-width roots and also doubled as side rails, so they
* additionally need placement and gutter migration.
*/
export async function migrateLegacySectionsToContainersAsync(db: Database) {
const existingBoards = await db.query.boards.findMany({
with: {
layouts: true,
sections: {
with: {
collapseStates: true,
layouts: true,
},
},
items: {
with: {
layouts: true,
},
},
},
});
let migratedSectionCount = 0;
for (const board of existingBoards) {
const categories = board.sections.filter((section) => (section.kind as string) === "category");
const legacyContainers = board.sections.filter((section) => (section.kind as string) === "dynamic");
if (categories.length === 0 && legacyContainers.length === 0) continue;
const operations: MigrationOperation[] = [];
const appendLegacyContainerOperations = () => {
for (const section of legacyContainers) {
operations.push(
updateSectionOperation(section.id, {
kind: "container",
xOffset: null,
yOffset: null,
name: null,
options: stringify(containerSectionOptionsSchema.parse(parse(section.options ?? emptySuperJSON))),
}),
);
migratedSectionCount += 1;
}
};
if (categories.length === 0) {
appendLegacyContainerOperations();
await executeBoardMigrationOperationsAsync(db, operations);
continue;
}
const categoryDetails = categories.map((category) => {
const options = legacyCategoryOptionsSchema.parse(parse(category.options ?? emptySuperJSON));
return {
category,
lane: options.railPlacement as BoardLane,
options,
};
});
const emptyRoots = board.sections.filter((section) => section.kind === "empty");
let mainRoot = emptyRoots.find((section) => getRootSectionLane(section.xOffset) === "main");
if (!mainRoot) {
const id = createId();
operations.push(
insertSectionsOperation([
{
id,
boardId: board.id,
kind: "empty",
xOffset: rootSectionOffsets.main,
yOffset: 0,
options: emptySuperJSON,
},
]),
);
mainRoot = {
id,
boardId: board.id,
kind: "empty",
xOffset: rootSectionOffsets.main,
yOffset: 0,
name: null,
options: emptySuperJSON,
collapseStates: [],
layouts: [],
};
emptyRoots.push(mainRoot);
}
const rootByLane = new Map<BoardLane, (typeof emptyRoots)[number]>([["main", mainRoot]]);
for (const lane of ["left", "right"] as const) {
const existing = emptyRoots.find((section) => getRootSectionLane(section.xOffset) === lane);
if (existing) {
rootByLane.set(lane, existing);
continue;
}
if (!categoryDetails.some((detail) => detail.lane === lane)) continue;
const id = createId();
operations.push(
insertSectionsOperation([
{
id,
boardId: board.id,
kind: "empty",
xOffset: rootSectionOffsets[lane],
yOffset: 0,
options: emptySuperJSON,
},
]),
);
rootByLane.set(lane, {
id,
boardId: board.id,
kind: "empty",
xOffset: rootSectionOffsets[lane],
yOffset: 0,
name: null,
options: emptySuperJSON,
collapseStates: [],
layouts: [],
});
}
for (const layout of board.layouts) {
const requestedLeft = Math.max(
0,
...categoryDetails.filter((detail) => detail.lane === "left").map((detail) => detail.options.columnCount),
);
const requestedRight = Math.max(
0,
...categoryDetails.filter((detail) => detail.lane === "right").map((detail) => detail.options.columnCount),
);
const left = Math.min(3, requestedLeft, Math.max(0, layout.columnCount - 1));
const right = Math.min(3, requestedRight, Math.max(0, layout.columnCount - left - 1));
const widths = {
left,
main: Math.max(1, layout.columnCount - left - right),
right,
} satisfies Record<BoardLane, number>;
operations.push(updateLayoutGuttersOperation(layout.id, left, right));
for (const lane of ["left", "main", "right"] as const) {
const targetRoot = rootByLane.get(lane);
const laneWidth = widths[lane];
if (!targetRoot || laneWidth === 0) continue;
const sourceRoots = emptyRoots
.filter((section) => getRootSectionLane(section.xOffset) === lane)
.toSorted((first, second) => (first.yOffset ?? 0) - (second.yOffset ?? 0));
const sourceBlocks = [
...sourceRoots.map((root) => ({
id: root.id,
yOffset: root.yOffset ?? 0,
type: "root" as const,
})),
...categoryDetails
.filter((detail) => detail.lane === lane)
.map((detail) => ({
id: detail.category.id,
yOffset: detail.category.yOffset ?? 0,
type: "category" as const,
detail,
})),
].toSorted(
(first, second) =>
first.yOffset - second.yOffset ||
(first.type === second.type ? first.id.localeCompare(second.id) : first.type === "category" ? -1 : 1),
);
let nextRow = 0;
for (const block of sourceBlocks) {
if (block.type === "category") {
const content = getDirectPlacements(board, layout.id, block.id);
const packedContent = packPlacements(content, laneWidth);
operations.push(...persistPlacementOperations(layout.id, block.id, packedContent));
const height = Math.max(1, getPlacementRowCount(packedContent));
const existingLayout = block.detail.category.layouts.find((candidate) => candidate.layoutId === layout.id);
const values = {
sectionId: block.id,
layoutId: layout.id,
parentSectionId: targetRoot.id,
xOffset: 0,
yOffset: nextRow,
width: laneWidth,
height,
};
if (existingLayout) {
operations.push(updateSectionLayoutOperation(block.id, layout.id, values));
} else {
operations.push(insertSectionLayoutOperation(values));
}
nextRow += height;
continue;
}
const content = getDirectPlacements(board, layout.id, block.id);
const relativePackedContent = packPlacements(content, laneWidth);
const packedContent = relativePackedContent.map((placement) => ({
...placement,
y: placement.y + nextRow,
}));
operations.push(...persistPlacementOperations(layout.id, targetRoot.id, packedContent));
nextRow += getPlacementRowCount(relativePackedContent);
}
}
}
for (const { category, options } of categoryDetails) {
const legacyCollapseStates = category.collapseStates;
for (const state of legacyCollapseStates) {
operations.push(updateCollapseStateOperation(category.id, state.userId, !state.collapsed));
}
operations.push(
updateSectionOperation(category.id, {
kind: "container",
xOffset: null,
yOffset: null,
name: null,
options: stringify(
containerSectionOptionsSchema.parse({
title: (category.name ?? "").slice(0, 64),
customCssClasses: [],
borderColor: "",
showLabel: options.showLabel,
collapsible: options.collapsible,
showOpenAll: options.showOpenAll,
}),
),
}),
);
migratedSectionCount += 1;
}
appendLegacyContainerOperations();
const retainedRootIds = new Set(Array.from(rootByLane.values(), (root) => root.id));
const obsoleteRootIds = emptyRoots.filter((root) => !retainedRootIds.has(root.id)).map((root) => root.id);
if (obsoleteRootIds.length > 0) {
operations.push(deleteSectionsOperation(obsoleteRootIds));
}
await executeBoardMigrationOperationsAsync(db, operations);
}
if (migratedSectionCount > 0) {
console.log(`Migrated legacy sections to containers count="${migratedSectionCount}"`);
}
}
const getDirectPlacements = (board: MigrationBoard, layoutId: string, sectionId: string): MigrationPlacement[] => [
...board.items.flatMap((item) => {
const layout = item.layouts.find(
(candidate) => candidate.layoutId === layoutId && candidate.sectionId === sectionId,
);
return layout
? [
{
id: item.id,
type: "item" as const,
x: layout.xOffset,
y: layout.yOffset,
w: layout.width,
h: layout.height,
},
]
: [];
}),
...board.sections.flatMap((section) => {
if (section.kind !== "dynamic" && section.kind !== "container") return [];
const layout = section.layouts.find(
(candidate) => candidate.layoutId === layoutId && candidate.parentSectionId === sectionId,
);
return layout
? [
{
id: section.id,
type: "section" as const,
x: layout.xOffset,
y: layout.yOffset,
w: layout.width,
h: layout.height,
},
]
: [];
}),
];
const executeBoardMigrationOperationsAsync = async (db: Database, operations: readonly MigrationOperation[]) => {
await handleTransactionsAsync(db, {
async handleAsync(transactionDb) {
await transactionDb.transaction(async (transaction) => {
for (const operation of operations) {
await operation.applyAsync(transaction as unknown as Database);
}
});
},
handleSync(sqliteDb) {
sqliteDb.transaction((transaction) => {
for (const operation of operations) {
operation.applySync(transaction);
}
});
},
});
};
const insertSectionsOperation = (values: (typeof sections.$inferInsert)[]): MigrationOperation => ({
async applyAsync(db) {
await db.insert(sections).values(values);
},
applySync(db) {
db.insert(sections).values(values).run();
},
});
const updateLayoutGuttersOperation = (
layoutId: string,
leftGutterColumnCount: number,
rightGutterColumnCount: number,
): MigrationOperation => ({
async applyAsync(db) {
await db.update(layouts).set({ leftGutterColumnCount, rightGutterColumnCount }).where(eq(layouts.id, layoutId));
},
applySync(db) {
db.update(layouts).set({ leftGutterColumnCount, rightGutterColumnCount }).where(eq(layouts.id, layoutId)).run();
},
});
const persistPlacementOperations = (
layoutId: string,
sectionId: string,
placements: readonly MigrationPlacement[],
): MigrationOperation[] =>
placements.map((placement) => {
const values = {
xOffset: placement.x,
yOffset: placement.y,
width: placement.w,
height: placement.h,
};
if (placement.type === "item") {
return {
async applyAsync(db) {
await db
.update(itemLayouts)
.set({ ...values, sectionId })
.where(and(eq(itemLayouts.itemId, placement.id), eq(itemLayouts.layoutId, layoutId)));
},
applySync(db) {
db.update(itemLayouts)
.set({ ...values, sectionId })
.where(and(eq(itemLayouts.itemId, placement.id), eq(itemLayouts.layoutId, layoutId)))
.run();
},
};
}
return {
async applyAsync(db) {
await db
.update(sectionLayouts)
.set({ ...values, parentSectionId: sectionId })
.where(and(eq(sectionLayouts.sectionId, placement.id), eq(sectionLayouts.layoutId, layoutId)));
},
applySync(db) {
db.update(sectionLayouts)
.set({ ...values, parentSectionId: sectionId })
.where(and(eq(sectionLayouts.sectionId, placement.id), eq(sectionLayouts.layoutId, layoutId)))
.run();
},
};
});
const updateSectionLayoutOperation = (
sectionId: string,
layoutId: string,
values: typeof sectionLayouts.$inferInsert,
): MigrationOperation => ({
async applyAsync(db) {
await db
.update(sectionLayouts)
.set(values)
.where(and(eq(sectionLayouts.sectionId, sectionId), eq(sectionLayouts.layoutId, layoutId)));
},
applySync(db) {
db.update(sectionLayouts)
.set(values)
.where(and(eq(sectionLayouts.sectionId, sectionId), eq(sectionLayouts.layoutId, layoutId)))
.run();
},
});
const insertSectionLayoutOperation = (values: typeof sectionLayouts.$inferInsert): MigrationOperation => ({
async applyAsync(db) {
await db.insert(sectionLayouts).values(values);
},
applySync(db) {
db.insert(sectionLayouts).values(values).run();
},
});
const updateCollapseStateOperation = (sectionId: string, userId: string, collapsed: boolean): MigrationOperation => ({
async applyAsync(db) {
await db
.update(sectionCollapseStates)
.set({ collapsed })
.where(and(eq(sectionCollapseStates.sectionId, sectionId), eq(sectionCollapseStates.userId, userId)));
},
applySync(db) {
db.update(sectionCollapseStates)
.set({ collapsed })
.where(and(eq(sectionCollapseStates.sectionId, sectionId), eq(sectionCollapseStates.userId, userId)))
.run();
},
});
const updateSectionOperation = (
sectionId: string,
values: Partial<typeof sections.$inferInsert>,
): MigrationOperation => ({
async applyAsync(db) {
await db.update(sections).set(values).where(eq(sections.id, sectionId));
},
applySync(db) {
db.update(sections).set(values).where(eq(sections.id, sectionId)).run();
},
});
const deleteSectionsOperation = (sectionIds: string[]): MigrationOperation => ({
async applyAsync(db) {
await db.delete(sections).where(inArray(sections.id, sectionIds));
},
applySync(db) {
db.delete(sections).where(inArray(sections.id, sectionIds)).run();
},
});
const packPlacements = (placements: readonly MigrationPlacement[], columnCount: number): MigrationPlacement[] => {
const placed: MigrationPlacement[] = [];
const candidates = placements
.map((placement) => ({
...placement,
x: Math.min(Math.max(0, placement.x), Math.max(0, columnCount - Math.min(columnCount, placement.w))),
y: Math.max(0, placement.y),
w: Math.min(columnCount, Math.max(1, placement.w)),
h: Math.max(1, placement.h),
}))
.toSorted((first, second) => first.y - second.y || first.x - second.x || first.id.localeCompare(second.id));
for (const candidate of candidates) {
let row = 0;
while (placed.some((placement) => overlaps({ ...candidate, y: row }, placement))) row += 1;
placed.push({ ...candidate, y: row });
}
return placed;
};
const overlaps = (first: MigrationPlacement, second: MigrationPlacement) =>
first.x < second.x + second.w &&
first.x + first.w > second.x &&
first.y < second.y + second.h &&
first.y + first.h > second.y;
const getPlacementRowCount = (placements: readonly MigrationPlacement[]) =>
Math.max(0, ...placements.map((placement) => placement.y + placement.h));
+2
View File
@@ -3,10 +3,12 @@ import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widge
import { migrateOpnsenseCredentialsAsync } from "./0001_opnsense_credentials";
import { migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync } from "./0002_app_widget_show_description_tooltip_to_display_mode";
import { migrateWidgetOnlyIntegrationsToOptionsAsync } from "./0003_remove_widget_only_integrations";
import { migrateLegacySectionsToContainersAsync } from "./0004_unify_sections_and_gutters";
export const applyCustomMigrationsAsync = async (db: Database) => {
await migrateReleaseWidgetProviderToOptionsAsync(db);
await migrateOpnsenseCredentialsAsync(db);
await migrateAppWidgetShowDescriptionTooltipToDisplayModeAsync(db);
await migrateWidgetOnlyIntegrationsToOptionsAsync(db);
await migrateLegacySectionsToContainersAsync(db);
};
@@ -0,0 +1,26 @@
CREATE TABLE `custom_widget_v2_definition` (
`id` varchar(64) NOT NULL,
`name` varchar(256) NOT NULL,
`description` text,
`icon_url` text,
`sources` text NOT NULL,
`requests` text NOT NULL,
`options` text NOT NULL,
`template` text NOT NULL,
`enabled` boolean DEFAULT true NOT NULL,
`created_at` timestamp DEFAULT (now()) NOT NULL,
`updated_at` timestamp DEFAULT (now()) NOT NULL,
`creator_id` varchar(64),
CONSTRAINT `custom_widget_v2_definition_id` PRIMARY KEY(`id`)
);--> statement-breakpoint
CREATE TABLE `custom_widget_v2_secret` (
`source_id` varchar(64) NOT NULL,
`kind` varchar(64) NOT NULL,
`encrypted_value` text NOT NULL,
`updated_at` timestamp NOT NULL,
`definition_id` varchar(64) NOT NULL,
CONSTRAINT `custom_widget_v2_secret_definition_id_source_id_kind_pk` PRIMARY KEY(`definition_id`,`source_id`,`kind`)
);--> statement-breakpoint
ALTER TABLE `custom_widget_v2_definition` ADD CONSTRAINT `custom_widget_v2_definition_creator_id_user_id_fk` FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `custom_widget_v2_secret` ADD CONSTRAINT `custom_widget_v2_secret_definition_id_fk` FOREIGN KEY (`definition_id`) REFERENCES `custom_widget_v2_definition`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
DELETE FROM `groupPermission` WHERE `permission` IN ('custom-widget-manage', 'custom-widget-secret-write');
@@ -0,0 +1,2 @@
ALTER TABLE `layout` ADD `left_gutter_column_count` tinyint DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `layout` ADD `right_gutter_column_count` tinyint DEFAULT 0 NOT NULL;
@@ -88,9 +88,7 @@
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -99,12 +97,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -112,10 +106,7 @@
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
]
"columns": ["provider", "provider_account_id"]
}
},
"uniqueConstraints": {},
@@ -152,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -165,9 +152,7 @@
"compositePrimaryKeys": {
"apiKey_id": {
"name": "apiKey_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -224,9 +209,7 @@
"compositePrimaryKeys": {
"app_id": {
"name": "app_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -263,12 +246,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -276,12 +255,8 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -289,11 +264,7 @@
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"name": "boardGroupPermission_board_id_group_id_permission_pk",
"columns": [
"board_id",
"group_id",
"permission"
]
"columns": ["board_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -330,12 +301,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -343,12 +310,8 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -356,11 +319,7 @@
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"name": "boardUserPermission_board_id_user_id_permission_pk",
"columns": [
"board_id",
"user_id",
"permission"
]
"columns": ["board_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -518,12 +477,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -531,17 +486,13 @@
"compositePrimaryKeys": {
"board_id": {
"name": "board_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {
"board_name_unique": {
"name": "board_name_unique",
"columns": [
"name"
]
"columns": ["name"]
}
},
"checkConstraint": {}
@@ -577,9 +528,7 @@
"compositePrimaryKeys": {
"cron_job_configuration_name": {
"name": "cron_job_configuration_name",
"columns": [
"name"
]
"columns": ["name"]
}
},
"uniqueConstraints": {},
@@ -609,12 +558,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -622,12 +567,8 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -635,10 +576,7 @@
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"name": "groupMember_group_id_user_id_pk",
"columns": [
"group_id",
"user_id"
]
"columns": ["group_id", "user_id"]
}
},
"uniqueConstraints": {},
@@ -668,12 +606,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -734,12 +668,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -747,12 +677,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -760,12 +686,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -773,17 +695,13 @@
"compositePrimaryKeys": {
"group_id": {
"name": "group_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {
"group_name_unique": {
"name": "group_name_unique",
"columns": [
"name"
]
"columns": ["name"]
}
},
"checkConstraint": {}
@@ -811,9 +729,7 @@
"compositePrimaryKeys": {
"iconRepository_id": {
"name": "iconRepository_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -864,12 +780,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -877,9 +789,7 @@
"compositePrimaryKeys": {
"icon_id": {
"name": "icon_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -916,12 +826,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -929,12 +835,8 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -942,11 +844,7 @@
"compositePrimaryKeys": {
"integration_group_permission__pk": {
"name": "integration_group_permission__pk",
"columns": [
"integration_id",
"group_id",
"permission"
]
"columns": ["integration_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -976,12 +874,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -989,12 +883,8 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1002,10 +892,7 @@
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"name": "integration_item_item_id_integration_id_pk",
"columns": [
"item_id",
"integration_id"
]
"columns": ["item_id", "integration_id"]
}
},
"uniqueConstraints": {},
@@ -1046,16 +933,12 @@
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": [
"updated_at"
],
"columns": ["updated_at"],
"isUnique": false
}
},
@@ -1064,12 +947,8 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1077,10 +956,7 @@
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"name": "integrationSecret_integration_id_kind_pk",
"columns": [
"integration_id",
"kind"
]
"columns": ["integration_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -1117,12 +993,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1130,12 +1002,8 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1143,11 +1011,7 @@
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"name": "integrationUserPermission_integration_id_user_id_permission_pk",
"columns": [
"integration_id",
"user_id",
"permission"
]
"columns": ["integration_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1195,9 +1059,7 @@
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
}
},
@@ -1206,12 +1068,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1219,9 +1077,7 @@
"compositePrimaryKeys": {
"integration_id": {
"name": "integration_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -1265,12 +1121,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1278,17 +1130,13 @@
"compositePrimaryKeys": {
"invite_id": {
"name": "invite_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {
"invite_token_unique": {
"name": "invite_token_unique",
"columns": [
"token"
]
"columns": ["token"]
}
},
"checkConstraint": {}
@@ -1352,12 +1200,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1365,12 +1209,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1378,12 +1218,8 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1391,11 +1227,7 @@
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"name": "item_layout_item_id_section_id_layout_id_pk",
"columns": [
"item_id",
"section_id",
"layout_id"
]
"columns": ["item_id", "section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -1448,12 +1280,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1461,9 +1289,7 @@
"compositePrimaryKeys": {
"item_id": {
"name": "item_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -1515,12 +1341,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1528,9 +1350,7 @@
"compositePrimaryKeys": {
"layout_id": {
"name": "layout_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -1596,12 +1416,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1609,9 +1425,7 @@
"compositePrimaryKeys": {
"media_id": {
"name": "media_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -1647,9 +1461,7 @@
"compositePrimaryKeys": {
"onboarding_id": {
"name": "onboarding_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -1722,12 +1534,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1735,17 +1543,13 @@
"compositePrimaryKeys": {
"search_engine_id": {
"name": "search_engine_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"columns": [
"short"
]
"columns": ["short"]
}
},
"checkConstraint": {}
@@ -1782,12 +1586,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1795,12 +1595,8 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1808,10 +1604,7 @@
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"name": "section_collapse_state_user_id_section_id_pk",
"columns": [
"user_id",
"section_id"
]
"columns": ["user_id", "section_id"]
}
},
"uniqueConstraints": {},
@@ -1876,12 +1669,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1889,12 +1678,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1902,12 +1687,8 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1915,10 +1696,7 @@
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"name": "section_layout_section_id_layout_id_pk",
"columns": [
"section_id",
"layout_id"
]
"columns": ["section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -1984,12 +1762,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1997,9 +1771,7 @@
"compositePrimaryKeys": {
"section_id": {
"name": "section_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -2029,17 +1801,13 @@
"compositePrimaryKeys": {
"serverSetting_setting_key": {
"name": "serverSetting_setting_key",
"columns": [
"setting_key"
]
"columns": ["setting_key"]
}
},
"uniqueConstraints": {
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"columns": [
"setting_key"
]
"columns": ["setting_key"]
}
},
"checkConstraint": {}
@@ -2072,9 +1840,7 @@
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -2083,12 +1849,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2096,9 +1858,7 @@
"compositePrimaryKeys": {
"session_session_token": {
"name": "session_session_token",
"columns": [
"session_token"
]
"columns": ["session_token"]
}
},
"uniqueConstraints": {},
@@ -2225,12 +1985,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2238,12 +1994,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2251,12 +2003,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2264,9 +2012,7 @@
"compositePrimaryKeys": {
"user_id": {
"name": "user_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -2302,10 +2048,7 @@
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"name": "verificationToken_identifier_token_pk",
"columns": [
"identifier",
"token"
]
"columns": ["identifier", "token"]
}
},
"uniqueConstraints": {},
@@ -2341,10 +2084,7 @@
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": [
"hostname",
"thumbprint"
]
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
@@ -2361,4 +2101,4 @@
"tables": {},
"indexes": {}
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -309,6 +309,20 @@
"when": 1783852800000,
"tag": "0043_add_right_click_preference",
"breakpoints": true
},
{
"idx": 44,
"version": "5",
"when": 1784305537309,
"tag": "0044_custom_widget_v2_tables",
"breakpoints": true
},
{
"idx": 45,
"version": "5",
"when": 1786212549933,
"tag": "0045_shiny_sir_ram",
"breakpoints": true
}
]
}
}
@@ -0,0 +1,25 @@
CREATE TABLE "custom_widget_v2_definition" (
"id" varchar(64) PRIMARY KEY NOT NULL,
"name" varchar(256) NOT NULL,
"description" text,
"icon_url" text,
"sources" text NOT NULL,
"requests" text NOT NULL,
"options" text NOT NULL,
"template" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"creator_id" varchar(64)
);--> statement-breakpoint
CREATE TABLE "custom_widget_v2_secret" (
"source_id" varchar(64) NOT NULL,
"kind" varchar(64) NOT NULL,
"encrypted_value" text NOT NULL,
"updated_at" timestamp NOT NULL,
"definition_id" varchar(64) NOT NULL,
CONSTRAINT "custom_widget_v2_secret_definition_id_source_id_kind_pk" PRIMARY KEY("definition_id","source_id","kind")
);--> statement-breakpoint
ALTER TABLE "custom_widget_v2_definition" ADD CONSTRAINT "custom_widget_v2_definition_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "custom_widget_v2_secret" ADD CONSTRAINT "custom_widget_v2_secret_definition_id_fk" FOREIGN KEY ("definition_id") REFERENCES "public"."custom_widget_v2_definition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
DELETE FROM "groupPermission" WHERE "permission" IN ('custom-widget-manage', 'custom-widget-secret-write');
@@ -0,0 +1,2 @@
ALTER TABLE "layout" ADD COLUMN "left_gutter_column_count" smallint DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "layout" ADD COLUMN "right_gutter_column_count" smallint DEFAULT 0 NOT NULL;
@@ -97,12 +97,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -110,10 +106,7 @@
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
]
"columns": ["provider", "provider_account_id"]
}
},
"uniqueConstraints": {},
@@ -150,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -244,12 +233,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -257,12 +242,8 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -270,11 +251,7 @@
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"name": "boardGroupPermission_board_id_group_id_permission_pk",
"columns": [
"board_id",
"group_id",
"permission"
]
"columns": ["board_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -311,12 +288,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -324,12 +297,8 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -337,11 +306,7 @@
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"name": "boardUserPermission_board_id_user_id_permission_pk",
"columns": [
"board_id",
"user_id",
"permission"
]
"columns": ["board_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -483,12 +448,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -498,9 +459,7 @@
"board_name_unique": {
"name": "board_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -646,12 +605,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -697,12 +652,8 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -710,10 +661,7 @@
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"name": "custom_widget_secret_definition_id_kind_pk",
"columns": [
"definition_id",
"kind"
]
"columns": ["definition_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -744,12 +692,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -757,12 +701,8 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -770,10 +710,7 @@
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"name": "groupMember_group_id_user_id_pk",
"columns": [
"group_id",
"user_id"
]
"columns": ["group_id", "user_id"]
}
},
"uniqueConstraints": {},
@@ -804,12 +741,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -867,12 +800,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -880,12 +809,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -893,12 +818,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -908,9 +829,7 @@
"group_name_unique": {
"name": "group_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -983,12 +902,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1028,12 +943,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1041,12 +952,8 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1054,11 +961,7 @@
"compositePrimaryKeys": {
"integration_group_permission__pk": {
"name": "integration_group_permission__pk",
"columns": [
"integration_id",
"group_id",
"permission"
]
"columns": ["integration_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1089,12 +992,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1102,12 +1001,8 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1115,10 +1010,7 @@
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"name": "integration_item_item_id_integration_id_pk",
"columns": [
"item_id",
"integration_id"
]
"columns": ["item_id", "integration_id"]
}
},
"uniqueConstraints": {},
@@ -1192,12 +1084,8 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1205,10 +1093,7 @@
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"name": "integrationSecret_integration_id_kind_pk",
"columns": [
"integration_id",
"kind"
]
"columns": ["integration_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -1245,12 +1130,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1258,12 +1139,8 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1271,11 +1148,7 @@
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"name": "integrationUserPermission_integration_id_user_id_permission_pk",
"columns": [
"integration_id",
"user_id",
"permission"
]
"columns": ["integration_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1340,12 +1213,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1391,12 +1260,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1406,9 +1271,7 @@
"invite_token_unique": {
"name": "invite_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -1468,12 +1331,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1481,12 +1340,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1494,12 +1349,8 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1507,11 +1358,7 @@
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"name": "item_layout_item_id_section_id_layout_id_pk",
"columns": [
"item_id",
"section_id",
"layout_id"
]
"columns": ["item_id", "section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -1562,12 +1409,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1620,12 +1463,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1690,12 +1529,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1797,12 +1632,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1812,9 +1643,7 @@
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"nullsNotDistinct": false,
"columns": [
"short"
]
"columns": ["short"]
}
},
"policies": {},
@@ -1851,12 +1680,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1864,12 +1689,8 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1877,10 +1698,7 @@
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"name": "section_collapse_state_user_id_section_id_pk",
"columns": [
"user_id",
"section_id"
]
"columns": ["user_id", "section_id"]
}
},
"uniqueConstraints": {},
@@ -1941,12 +1759,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1954,12 +1768,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1967,12 +1777,8 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1980,10 +1786,7 @@
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"name": "section_layout_section_id_layout_id_pk",
"columns": [
"section_id",
"layout_id"
]
"columns": ["section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -2045,12 +1848,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2086,9 +1885,7 @@
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"nullsNotDistinct": false,
"columns": [
"setting_key"
]
"columns": ["setting_key"]
}
},
"policies": {},
@@ -2140,12 +1937,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2277,12 +2070,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2290,12 +2079,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2303,12 +2088,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2347,10 +2128,7 @@
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"name": "verificationToken_identifier_token_pk",
"columns": [
"identifier",
"token"
]
"columns": ["identifier", "token"]
}
},
"uniqueConstraints": {},
@@ -2386,10 +2164,7 @@
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": [
"hostname",
"thumbprint"
]
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
@@ -2409,4 +2184,4 @@
"schemas": {},
"tables": {}
}
}
}
@@ -97,12 +97,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -110,10 +106,7 @@
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
]
"columns": ["provider", "provider_account_id"]
}
},
"uniqueConstraints": {},
@@ -150,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -244,12 +233,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -257,12 +242,8 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -270,11 +251,7 @@
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"name": "boardGroupPermission_board_id_group_id_permission_pk",
"columns": [
"board_id",
"group_id",
"permission"
]
"columns": ["board_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -311,12 +288,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -324,12 +297,8 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -337,11 +306,7 @@
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"name": "boardUserPermission_board_id_user_id_permission_pk",
"columns": [
"board_id",
"user_id",
"permission"
]
"columns": ["board_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -483,12 +448,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -498,9 +459,7 @@
"board_name_unique": {
"name": "board_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -647,12 +606,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -698,12 +653,8 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -711,10 +662,7 @@
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"name": "custom_widget_secret_definition_id_kind_pk",
"columns": [
"definition_id",
"kind"
]
"columns": ["definition_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -745,12 +693,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -758,12 +702,8 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -771,10 +711,7 @@
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"name": "groupMember_group_id_user_id_pk",
"columns": [
"group_id",
"user_id"
]
"columns": ["group_id", "user_id"]
}
},
"uniqueConstraints": {},
@@ -805,12 +742,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -868,12 +801,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -881,12 +810,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -894,12 +819,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -909,9 +830,7 @@
"group_name_unique": {
"name": "group_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -984,12 +903,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1029,12 +944,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1042,12 +953,8 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1055,11 +962,7 @@
"compositePrimaryKeys": {
"integration_group_permission__pk": {
"name": "integration_group_permission__pk",
"columns": [
"integration_id",
"group_id",
"permission"
]
"columns": ["integration_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1090,12 +993,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1103,12 +1002,8 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1116,10 +1011,7 @@
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"name": "integration_item_item_id_integration_id_pk",
"columns": [
"item_id",
"integration_id"
]
"columns": ["item_id", "integration_id"]
}
},
"uniqueConstraints": {},
@@ -1193,12 +1085,8 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1206,10 +1094,7 @@
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"name": "integrationSecret_integration_id_kind_pk",
"columns": [
"integration_id",
"kind"
]
"columns": ["integration_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -1246,12 +1131,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1259,12 +1140,8 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1272,11 +1149,7 @@
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"name": "integrationUserPermission_integration_id_user_id_permission_pk",
"columns": [
"integration_id",
"user_id",
"permission"
]
"columns": ["integration_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1341,12 +1214,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1392,12 +1261,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1407,9 +1272,7 @@
"invite_token_unique": {
"name": "invite_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -1469,12 +1332,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1482,12 +1341,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1495,12 +1350,8 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1508,11 +1359,7 @@
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"name": "item_layout_item_id_section_id_layout_id_pk",
"columns": [
"item_id",
"section_id",
"layout_id"
]
"columns": ["item_id", "section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -1563,12 +1410,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1621,12 +1464,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1691,12 +1530,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1798,12 +1633,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1813,9 +1644,7 @@
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"nullsNotDistinct": false,
"columns": [
"short"
]
"columns": ["short"]
}
},
"policies": {},
@@ -1852,12 +1681,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1865,12 +1690,8 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1878,10 +1699,7 @@
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"name": "section_collapse_state_user_id_section_id_pk",
"columns": [
"user_id",
"section_id"
]
"columns": ["user_id", "section_id"]
}
},
"uniqueConstraints": {},
@@ -1942,12 +1760,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1955,12 +1769,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1968,12 +1778,8 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1981,10 +1787,7 @@
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"name": "section_layout_section_id_layout_id_pk",
"columns": [
"section_id",
"layout_id"
]
"columns": ["section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -2046,12 +1849,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2087,9 +1886,7 @@
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"nullsNotDistinct": false,
"columns": [
"setting_key"
]
"columns": ["setting_key"]
}
},
"policies": {},
@@ -2141,12 +1938,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2278,12 +2071,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2291,12 +2080,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2304,12 +2089,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2348,10 +2129,7 @@
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"name": "verificationToken_identifier_token_pk",
"columns": [
"identifier",
"token"
]
"columns": ["identifier", "token"]
}
},
"uniqueConstraints": {},
@@ -2387,10 +2165,7 @@
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": [
"hostname",
"thumbprint"
]
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
@@ -97,12 +97,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -110,10 +106,7 @@
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
]
"columns": ["provider", "provider_account_id"]
}
},
"uniqueConstraints": {},
@@ -150,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -244,12 +233,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -257,12 +242,8 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -270,11 +251,7 @@
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"name": "boardGroupPermission_board_id_group_id_permission_pk",
"columns": [
"board_id",
"group_id",
"permission"
]
"columns": ["board_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -311,12 +288,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -324,12 +297,8 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -337,11 +306,7 @@
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"name": "boardUserPermission_board_id_user_id_permission_pk",
"columns": [
"board_id",
"user_id",
"permission"
]
"columns": ["board_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -483,12 +448,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -498,9 +459,7 @@
"board_name_unique": {
"name": "board_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -647,12 +606,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -698,12 +653,8 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -711,10 +662,7 @@
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"name": "custom_widget_secret_definition_id_kind_pk",
"columns": [
"definition_id",
"kind"
]
"columns": ["definition_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -745,12 +693,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -758,12 +702,8 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -771,10 +711,7 @@
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"name": "groupMember_group_id_user_id_pk",
"columns": [
"group_id",
"user_id"
]
"columns": ["group_id", "user_id"]
}
},
"uniqueConstraints": {},
@@ -805,12 +742,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -868,12 +801,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -881,12 +810,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -894,12 +819,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -909,9 +830,7 @@
"group_name_unique": {
"name": "group_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
"columns": ["name"]
}
},
"policies": {},
@@ -984,12 +903,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1029,12 +944,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1042,12 +953,8 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1055,11 +962,7 @@
"compositePrimaryKeys": {
"integration_group_permission__pk": {
"name": "integration_group_permission__pk",
"columns": [
"integration_id",
"group_id",
"permission"
]
"columns": ["integration_id", "group_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1090,12 +993,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1103,12 +1002,8 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1116,10 +1011,7 @@
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"name": "integration_item_item_id_integration_id_pk",
"columns": [
"item_id",
"integration_id"
]
"columns": ["item_id", "integration_id"]
}
},
"uniqueConstraints": {},
@@ -1193,12 +1085,8 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1206,10 +1094,7 @@
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"name": "integrationSecret_integration_id_kind_pk",
"columns": [
"integration_id",
"kind"
]
"columns": ["integration_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -1246,12 +1131,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1259,12 +1140,8 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1272,11 +1149,7 @@
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"name": "integrationUserPermission_integration_id_user_id_permission_pk",
"columns": [
"integration_id",
"user_id",
"permission"
]
"columns": ["integration_id", "user_id", "permission"]
}
},
"uniqueConstraints": {},
@@ -1341,12 +1214,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1392,12 +1261,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1407,9 +1272,7 @@
"invite_token_unique": {
"name": "invite_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -1469,12 +1332,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1482,12 +1341,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1495,12 +1350,8 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1508,11 +1359,7 @@
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"name": "item_layout_item_id_section_id_layout_id_pk",
"columns": [
"item_id",
"section_id",
"layout_id"
]
"columns": ["item_id", "section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -1563,12 +1410,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1621,12 +1464,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1691,12 +1530,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1798,12 +1633,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1813,9 +1644,7 @@
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"nullsNotDistinct": false,
"columns": [
"short"
]
"columns": ["short"]
}
},
"policies": {},
@@ -1852,12 +1681,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1865,12 +1690,8 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1878,10 +1699,7 @@
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"name": "section_collapse_state_user_id_section_id_pk",
"columns": [
"user_id",
"section_id"
]
"columns": ["user_id", "section_id"]
}
},
"uniqueConstraints": {},
@@ -1942,12 +1760,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1955,12 +1769,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1968,12 +1778,8 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1981,10 +1787,7 @@
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"name": "section_layout_section_id_layout_id_pk",
"columns": [
"section_id",
"layout_id"
]
"columns": ["section_id", "layout_id"]
}
},
"uniqueConstraints": {},
@@ -2046,12 +1849,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2087,9 +1886,7 @@
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"nullsNotDistinct": false,
"columns": [
"setting_key"
]
"columns": ["setting_key"]
}
},
"policies": {},
@@ -2141,12 +1938,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2278,12 +2071,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2291,12 +2080,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2304,12 +2089,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2348,10 +2129,7 @@
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"name": "verificationToken_identifier_token_pk",
"columns": [
"identifier",
"token"
]
"columns": ["identifier", "token"]
}
},
"uniqueConstraints": {},
@@ -2387,10 +2165,7 @@
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": [
"hostname",
"thumbprint"
]
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
@@ -2434,22 +2209,15 @@
"tableFrom": "widget_secret",
"tableTo": "item",
"schemaTo": "public",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"widget_secret_item_id_kind_pk": {
"columns": [
"item_id",
"kind"
],
"columns": ["item_id", "kind"],
"name": "widget_secret_item_id_kind_pk"
}
},
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -71,6 +71,20 @@
"when": 1783786677702,
"tag": "0009_add_right_click_preference",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1784305412862,
"tag": "0010_custom_widget_v2_tables",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1786212457549,
"tag": "0011_futuristic_valkyrie",
"breakpoints": true
}
]
}
}
+18 -115
View File
@@ -1,7 +1,7 @@
import SuperJSON from "superjson";
import { createId, objectKeys } from "@homarr/common";
import { customWidgetImportSchema } from "@homarr/validation/custom-widget";
import { BUNDLED_CUSTOM_WIDGETS, customWidgetDefinitionSchema } from "@homarr/custom-widgets/core";
import {
createDocumentationLink,
credentialsAdminGroup,
@@ -27,6 +27,7 @@ import {
import {
apps,
boards,
customWidgetDefinitions,
groupMembers,
groupPermissions,
groups,
@@ -39,102 +40,11 @@ import {
searchEngines,
sections,
users,
customWidgetDefinitions,
} from "../schema";
import type { Integration } from "../schema";
const isTruthyEnv = (value: string | undefined) => ["1", "yes", "t", "true"].includes((value ?? "").toLowerCase());
const CUSTOM_WIDGET_SEEDS: Array<{ id: string; data: Record<string, unknown> }> = [
{
id: "seed-dog-facts",
data: {
$schema: "homarr-custom-widget-v2",
name: "Random Dog Fact",
description: "Displays a random fun fact about dogs",
url: "https://dogapi.dog/api/v2/facts",
authType: "none",
method: "GET",
displayType: "singleValue",
displayConfig: {
type: "singleValue",
jsonPath: "$.data[0].attributes.body",
label: "Dog Fact",
unit: "",
valueSize: "sm",
labelPosition: "above",
},
},
},
{
id: "seed-currency-exchange",
data: {
$schema: "homarr-custom-widget-v2",
name: "Currency Exchange (JPY)",
description: "Converts 50 Japanese Yen to EUR and USD using European Central Bank rates",
url: "https://api.frankfurter.dev/v1/latest?from=JPY&to=EUR,USD&amount=50",
authType: "none",
method: "GET",
displayType: "keyValue",
displayConfig: {
type: "keyValue",
mappings: [
{ label: "50 JPY → EUR", jsonPath: "$.rates.EUR", unit: "€" },
{ label: "50 JPY → USD", jsonPath: "$.rates.USD", unit: "$" },
],
layout: "list",
columns: 2,
},
},
},
{
id: "seed-jellyfin",
data: {
$schema: "homarr-custom-widget-v2",
name: "Jellyfin library",
description: "Counts the number of movies, series, episodes and songs in the library",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
url: "https://jellyfin.homelab.com/Items/Counts",
authType: "apiKeyHeader",
headerName: "X-Emby-Token",
method: "GET",
requestBody: null,
displayType: "countGrid",
displayConfig: {
type: "countGrid",
items: [
{ label: "Movies", jsonPath: "$.MovieCount", unit: "" },
{ label: "Series", jsonPath: "$.SeriesCount", unit: "" },
{ label: "Episodes", jsonPath: "$.EpisodeCount", unit: "" },
{ label: "Songs", jsonPath: "$.SongCount", unit: "" },
],
columns: 4,
valueSize: "lg",
},
},
},
{
id: "seed-pokedex",
data: {
$schema: "homarr-custom-widget-v2",
name: "Pokédex",
description: "Browseable Pokémon list",
iconUrl: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png",
url: "https://pokeapi.co/api/v2/pokemon?limit=75",
authType: "none",
headerName: null,
method: "GET",
requestBody: null,
displayType: "customJsx",
displayConfig: {
type: "customJsx",
template:
'<Stack gap="md" p="xs">\n <Card\n withBorder\n radius="xl"\n p="md"\n shadow="md"\n style={{\n background: "linear-gradient(135deg, rgba(250,82,82,0.22), rgba(253,126,20,0.10), rgba(255,255,255,0.03))",\n border: "1px solid rgba(250,82,82,0.35)",\n overflow: "hidden"\n }}\n >\n <Group justify="space-between" wrap="nowrap">\n <Stack gap={2}>\n <Title order={3}>Pokédex</Title>\n </Stack>\n </Group>\n </Card>\n\n <PaginatedList pageSize={12}>\n <Grid gutter="sm">\n {data.results.map((pokemon, i) =>\n <Grid.Col span={1.5}>\n <Anchor href={pokemon.url} target="_blank" underline="never">\n <Card\n withBorder\n radius="xl"\n p="xs"\n shadow="md"\n style={{\n cursor: "pointer",\n position: "relative",\n overflow: "hidden",\n minHeight: 190,\n background: "linear-gradient(160deg, rgba(255,255,255,0.12), rgba(250,82,82,0.10), rgba(0,0,0,0.04))",\n border: "1px solid rgba(250,82,82,0.28)",\n transition: "transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background 180ms ease"\n }}\n >\n <Stack gap="sm" align="center">\n <Group justify="space-between" wrap="nowrap" style={{ width: "100%" }}>\n <Badge size="sm" color="red" variant="filled">\n #{String(i + 1).padStart(3, "0")}\n </Badge>\n <Text fw={800} tt="capitalize" ta="right" truncate style={{ maxWidth: 120 }}>\n {pokemon.name}\n </Text>\n </Group>\n\n <Paper\n radius="xl"\n p="xs"\n withBorder\n style={{\n background: "radial-gradient(circle, rgba(255,255,255,0.95), rgba(250,82,82,0.16))",\n border: "1px solid rgba(255,255,255,0.45)",\n boxShadow: "inset 0 0 20px rgba(255,255,255,0.25)"\n }}\n >\n <Avatar\n src={"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" + String(i + 1) + ".png"}\n alt={pokemon.name}\n size={92}\n radius="xl"\n style={{\n transition: "transform 180ms ease",\n filter: "drop-shadow(0 8px 10px rgba(0,0,0,0.25))"\n }}\n />\n </Paper>\n </Stack>\n </Card>\n </Anchor>\n </Grid.Col>\n )}\n </Grid>\n </PaginatedList>\n</Stack>',
},
},
},
];
export const seedDataAsync = async (db: Database) => {
if (isTruthyEnv(process.env.UNSAFE_ENABLE_MOCK_INTEGRATION)) {
console.warn(
@@ -646,40 +556,33 @@ const seedDemoUserAsync = async (db: Database) => {
};
const seedDefaultCustomWidgetsAsync = async (db: Database) => {
const seedIds = CUSTOM_WIDGET_SEEDS.map((s) => s.id);
const seedIds = BUNDLED_CUSTOM_WIDGETS.map(({ id }) => id);
const existing = await db.query.customWidgetDefinitions.findMany({
columns: { id: true },
where: inArray(customWidgetDefinitions.id, seedIds),
});
const existingIds = new Set(existing.map((row) => row.id));
const seedValues = CUSTOM_WIDGET_SEEDS.filter((seed) => !existingIds.has(seed.id)).map((seed) => {
const parsed = customWidgetImportSchema.parse(seed.data);
const existingIds = new Set(existing.map(({ id }) => id));
const values = BUNDLED_CUSTOM_WIDGETS.filter(({ id }) => !existingIds.has(id)).map(({ id, widget }) => {
const definition = customWidgetDefinitionSchema.parse(widget);
return {
id: seed.id,
name: parsed.name,
description: parsed.description ?? null,
iconUrl: parsed.iconUrl ?? null,
url: parsed.url,
authType: parsed.authType,
headerName: parsed.headerName ?? null,
method: parsed.method,
requestBody: parsed.requestBody ?? null,
displayType: parsed.displayType,
displayConfig: SuperJSON.stringify(parsed.displayConfig),
id,
name: definition.name,
description: definition.description ?? null,
iconUrl: definition.iconUrl ?? null,
sources: SuperJSON.stringify(definition.sources),
requests: SuperJSON.stringify(definition.requests),
options: SuperJSON.stringify(definition.options),
template: definition.template,
enabled: false,
creatorId: null,
};
});
if (seedValues.length === 0) {
console.log("Skipping seeding of default custom widgets as they already exist");
if (values.length === 0) {
console.log("Skipping seeding of bundled custom widgets because they already exist");
return;
}
await db.insert(customWidgetDefinitions).values(seedValues);
console.log(`Created ${seedValues.length} default custom widgets through seeding process`);
await db.insert(customWidgetDefinitions).values(values);
console.log(`Created ${values.length} bundled custom widgets through seeding process`);
};
const seedBoardWidgetsAsync = async (db: Database) => {
@@ -0,0 +1,25 @@
CREATE TABLE `custom_widget_v2_definition` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`description` text,
`icon_url` text,
`sources` text NOT NULL,
`requests` text NOT NULL,
`options` text NOT NULL,
`template` text NOT NULL,
`enabled` integer DEFAULT true NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
`creator_id` text,
FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null
);--> statement-breakpoint
CREATE TABLE `custom_widget_v2_secret` (
`source_id` text NOT NULL,
`kind` text NOT NULL,
`encrypted_value` text NOT NULL,
`updated_at` integer NOT NULL,
`definition_id` text NOT NULL,
PRIMARY KEY(`definition_id`, `source_id`, `kind`),
FOREIGN KEY (`definition_id`) REFERENCES `custom_widget_v2_definition`(`id`) ON UPDATE no action ON DELETE cascade
);--> statement-breakpoint
DELETE FROM `groupPermission` WHERE `permission` IN ('custom-widget-manage', 'custom-widget-secret-write');
@@ -0,0 +1,2 @@
ALTER TABLE `layout` ADD `left_gutter_column_count` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `layout` ADD `right_gutter_column_count` integer DEFAULT 0 NOT NULL;
@@ -88,9 +88,7 @@
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -99,22 +97,15 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"columns": [
"provider",
"provider_account_id"
],
"columns": ["provider", "provider_account_id"],
"name": "account_provider_provider_account_id_pk"
}
},
@@ -152,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -249,12 +236,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -262,23 +245,15 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"columns": [
"board_id",
"group_id",
"permission"
],
"columns": ["board_id", "group_id", "permission"],
"name": "boardGroupPermission_board_id_group_id_permission_pk"
}
},
@@ -316,12 +291,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -329,23 +300,15 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"columns": [
"board_id",
"user_id",
"permission"
],
"columns": ["board_id", "user_id", "permission"],
"name": "boardUserPermission_board_id_user_id_permission_pk"
}
},
@@ -501,9 +464,7 @@
"indexes": {
"board_name_unique": {
"name": "board_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -512,12 +473,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -679,12 +636,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -731,22 +684,15 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"columns": [
"definition_id",
"kind"
],
"columns": ["definition_id", "kind"],
"name": "custom_widget_secret_definition_id_kind_pk"
}
},
@@ -777,12 +723,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -790,22 +732,15 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"columns": [
"group_id",
"user_id"
],
"columns": ["group_id", "user_id"],
"name": "groupMember_group_id_user_id_pk"
}
},
@@ -836,12 +771,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -899,9 +830,7 @@
"indexes": {
"group_name_unique": {
"name": "group_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -910,12 +839,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -923,12 +848,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -936,12 +857,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1019,12 +936,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1064,12 +977,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1077,23 +986,15 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationGroupPermissions_integration_id_group_id_permission_pk": {
"columns": [
"integration_id",
"group_id",
"permission"
],
"columns": ["integration_id", "group_id", "permission"],
"name": "integrationGroupPermissions_integration_id_group_id_permission_pk"
}
},
@@ -1124,12 +1025,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1137,22 +1034,15 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"columns": [
"item_id",
"integration_id"
],
"columns": ["item_id", "integration_id"],
"name": "integration_item_item_id_integration_id_pk"
}
},
@@ -1194,16 +1084,12 @@
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": [
"updated_at"
],
"columns": ["updated_at"],
"isUnique": false
}
},
@@ -1212,22 +1098,15 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"columns": [
"integration_id",
"kind"
],
"columns": ["integration_id", "kind"],
"name": "integrationSecret_integration_id_kind_pk"
}
},
@@ -1265,12 +1144,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1278,23 +1153,15 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"columns": [
"integration_id",
"user_id",
"permission"
],
"columns": ["integration_id", "user_id", "permission"],
"name": "integrationUserPermission_integration_id_user_id_permission_pk"
}
},
@@ -1343,9 +1210,7 @@
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
}
},
@@ -1354,12 +1219,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1403,9 +1264,7 @@
"indexes": {
"invite_token_unique": {
"name": "invite_token_unique",
"columns": [
"token"
],
"columns": ["token"],
"isUnique": true
}
},
@@ -1414,12 +1273,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1487,12 +1342,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1500,12 +1351,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1513,23 +1360,15 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"columns": [
"item_id",
"section_id",
"layout_id"
],
"columns": ["item_id", "section_id", "layout_id"],
"name": "item_layout_item_id_section_id_layout_id_pk"
}
},
@@ -1583,12 +1422,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1643,12 +1478,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1717,12 +1548,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1826,9 +1653,7 @@
"indexes": {
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"columns": [
"short"
],
"columns": ["short"],
"isUnique": true
}
},
@@ -1837,12 +1662,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1883,12 +1704,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1896,22 +1713,15 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"columns": [
"user_id",
"section_id"
],
"columns": ["user_id", "section_id"],
"name": "section_collapse_state_user_id_section_id_pk"
}
},
@@ -1977,12 +1787,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1990,12 +1796,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2003,22 +1805,15 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"columns": [
"section_id",
"layout_id"
],
"columns": ["section_id", "layout_id"],
"name": "section_layout_section_id_layout_id_pk"
}
},
@@ -2085,12 +1880,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2121,9 +1912,7 @@
"indexes": {
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"columns": [
"setting_key"
],
"columns": ["setting_key"],
"isUnique": true
}
},
@@ -2160,9 +1949,7 @@
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -2171,12 +1958,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2214,10 +1997,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"columns": [
"hostname",
"thumbprint"
],
"columns": ["hostname", "thumbprint"],
"name": "trusted_certificate_hostname_hostname_thumbprint_pk"
}
},
@@ -2361,12 +2141,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2374,12 +2150,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2387,12 +2159,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2430,10 +2198,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": [
"identifier",
"token"
],
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
@@ -2451,4 +2216,4 @@
"internal": {
"indexes": {}
}
}
}
@@ -88,9 +88,7 @@
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -99,22 +97,15 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"columns": [
"provider",
"provider_account_id"
],
"columns": ["provider", "provider_account_id"],
"name": "account_provider_provider_account_id_pk"
}
},
@@ -152,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -249,12 +236,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -262,23 +245,15 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"columns": [
"board_id",
"group_id",
"permission"
],
"columns": ["board_id", "group_id", "permission"],
"name": "boardGroupPermission_board_id_group_id_permission_pk"
}
},
@@ -316,12 +291,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -329,23 +300,15 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"columns": [
"board_id",
"user_id",
"permission"
],
"columns": ["board_id", "user_id", "permission"],
"name": "boardUserPermission_board_id_user_id_permission_pk"
}
},
@@ -501,9 +464,7 @@
"indexes": {
"board_name_unique": {
"name": "board_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -512,12 +473,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -680,12 +637,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -732,22 +685,15 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"columns": [
"definition_id",
"kind"
],
"columns": ["definition_id", "kind"],
"name": "custom_widget_secret_definition_id_kind_pk"
}
},
@@ -778,12 +724,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -791,22 +733,15 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"columns": [
"group_id",
"user_id"
],
"columns": ["group_id", "user_id"],
"name": "groupMember_group_id_user_id_pk"
}
},
@@ -837,12 +772,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -900,9 +831,7 @@
"indexes": {
"group_name_unique": {
"name": "group_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -911,12 +840,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -924,12 +849,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -937,12 +858,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1020,12 +937,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1065,12 +978,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1078,23 +987,15 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationGroupPermissions_integration_id_group_id_permission_pk": {
"columns": [
"integration_id",
"group_id",
"permission"
],
"columns": ["integration_id", "group_id", "permission"],
"name": "integrationGroupPermissions_integration_id_group_id_permission_pk"
}
},
@@ -1125,12 +1026,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1138,22 +1035,15 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"columns": [
"item_id",
"integration_id"
],
"columns": ["item_id", "integration_id"],
"name": "integration_item_item_id_integration_id_pk"
}
},
@@ -1195,16 +1085,12 @@
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": [
"updated_at"
],
"columns": ["updated_at"],
"isUnique": false
}
},
@@ -1213,22 +1099,15 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"columns": [
"integration_id",
"kind"
],
"columns": ["integration_id", "kind"],
"name": "integrationSecret_integration_id_kind_pk"
}
},
@@ -1266,12 +1145,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1279,23 +1154,15 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"columns": [
"integration_id",
"user_id",
"permission"
],
"columns": ["integration_id", "user_id", "permission"],
"name": "integrationUserPermission_integration_id_user_id_permission_pk"
}
},
@@ -1344,9 +1211,7 @@
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
}
},
@@ -1355,12 +1220,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1404,9 +1265,7 @@
"indexes": {
"invite_token_unique": {
"name": "invite_token_unique",
"columns": [
"token"
],
"columns": ["token"],
"isUnique": true
}
},
@@ -1415,12 +1274,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1488,12 +1343,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1501,12 +1352,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1514,23 +1361,15 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"columns": [
"item_id",
"section_id",
"layout_id"
],
"columns": ["item_id", "section_id", "layout_id"],
"name": "item_layout_item_id_section_id_layout_id_pk"
}
},
@@ -1584,12 +1423,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1644,12 +1479,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1718,12 +1549,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1827,9 +1654,7 @@
"indexes": {
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"columns": [
"short"
],
"columns": ["short"],
"isUnique": true
}
},
@@ -1838,12 +1663,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1884,12 +1705,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1897,22 +1714,15 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"columns": [
"user_id",
"section_id"
],
"columns": ["user_id", "section_id"],
"name": "section_collapse_state_user_id_section_id_pk"
}
},
@@ -1978,12 +1788,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1991,12 +1797,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2004,22 +1806,15 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"columns": [
"section_id",
"layout_id"
],
"columns": ["section_id", "layout_id"],
"name": "section_layout_section_id_layout_id_pk"
}
},
@@ -2086,12 +1881,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2122,9 +1913,7 @@
"indexes": {
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"columns": [
"setting_key"
],
"columns": ["setting_key"],
"isUnique": true
}
},
@@ -2161,9 +1950,7 @@
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -2172,12 +1959,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2215,10 +1998,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"columns": [
"hostname",
"thumbprint"
],
"columns": ["hostname", "thumbprint"],
"name": "trusted_certificate_hostname_hostname_thumbprint_pk"
}
},
@@ -2362,12 +2142,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2375,12 +2151,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2388,12 +2160,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2431,10 +2199,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": [
"identifier",
"token"
],
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
@@ -88,9 +88,7 @@
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -99,22 +97,15 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_provider_account_id_pk": {
"columns": [
"provider",
"provider_account_id"
],
"columns": ["provider", "provider_account_id"],
"name": "account_provider_provider_account_id_pk"
}
},
@@ -152,12 +143,8 @@
"name": "apiKey_user_id_user_id_fk",
"tableFrom": "apiKey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -249,12 +236,8 @@
"name": "boardGroupPermission_board_id_board_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -262,23 +245,15 @@
"name": "boardGroupPermission_group_id_group_id_fk",
"tableFrom": "boardGroupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardGroupPermission_board_id_group_id_permission_pk": {
"columns": [
"board_id",
"group_id",
"permission"
],
"columns": ["board_id", "group_id", "permission"],
"name": "boardGroupPermission_board_id_group_id_permission_pk"
}
},
@@ -316,12 +291,8 @@
"name": "boardUserPermission_board_id_board_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -329,23 +300,15 @@
"name": "boardUserPermission_user_id_user_id_fk",
"tableFrom": "boardUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardUserPermission_board_id_user_id_permission_pk": {
"columns": [
"board_id",
"user_id",
"permission"
],
"columns": ["board_id", "user_id", "permission"],
"name": "boardUserPermission_board_id_user_id_permission_pk"
}
},
@@ -501,9 +464,7 @@
"indexes": {
"board_name_unique": {
"name": "board_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -512,12 +473,8 @@
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -680,12 +637,8 @@
"name": "custom_widget_definition_creator_id_user_id_fk",
"tableFrom": "custom_widget_definition",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -732,22 +685,15 @@
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": [
"definition_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["definition_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_kind_pk": {
"columns": [
"definition_id",
"kind"
],
"columns": ["definition_id", "kind"],
"name": "custom_widget_secret_definition_id_kind_pk"
}
},
@@ -778,12 +724,8 @@
"name": "groupMember_group_id_group_id_fk",
"tableFrom": "groupMember",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -791,22 +733,15 @@
"name": "groupMember_user_id_user_id_fk",
"tableFrom": "groupMember",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"groupMember_group_id_user_id_pk": {
"columns": [
"group_id",
"user_id"
],
"columns": ["group_id", "user_id"],
"name": "groupMember_group_id_user_id_pk"
}
},
@@ -837,12 +772,8 @@
"name": "groupPermission_group_id_group_id_fk",
"tableFrom": "groupPermission",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -900,9 +831,7 @@
"indexes": {
"group_name_unique": {
"name": "group_name_unique",
"columns": [
"name"
],
"columns": ["name"],
"isUnique": true
}
},
@@ -911,12 +840,8 @@
"name": "group_owner_id_user_id_fk",
"tableFrom": "group",
"tableTo": "user",
"columnsFrom": [
"owner_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["owner_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -924,12 +849,8 @@
"name": "group_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -937,12 +858,8 @@
"name": "group_mobile_home_board_id_board_id_fk",
"tableFrom": "group",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1020,12 +937,8 @@
"name": "icon_icon_repository_id_iconRepository_id_fk",
"tableFrom": "icon",
"tableTo": "iconRepository",
"columnsFrom": [
"icon_repository_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["icon_repository_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1065,12 +978,8 @@
"name": "integrationGroupPermissions_integration_id_integration_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1078,23 +987,15 @@
"name": "integrationGroupPermissions_group_id_group_id_fk",
"tableFrom": "integrationGroupPermissions",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["group_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationGroupPermissions_integration_id_group_id_permission_pk": {
"columns": [
"integration_id",
"group_id",
"permission"
],
"columns": ["integration_id", "group_id", "permission"],
"name": "integrationGroupPermissions_integration_id_group_id_permission_pk"
}
},
@@ -1125,12 +1026,8 @@
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1138,22 +1035,15 @@
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"columns": [
"item_id",
"integration_id"
],
"columns": ["item_id", "integration_id"],
"name": "integration_item_item_id_integration_id_pk"
}
},
@@ -1195,16 +1085,12 @@
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": [
"updated_at"
],
"columns": ["updated_at"],
"isUnique": false
}
},
@@ -1213,22 +1099,15 @@
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"columns": [
"integration_id",
"kind"
],
"columns": ["integration_id", "kind"],
"name": "integrationSecret_integration_id_kind_pk"
}
},
@@ -1266,12 +1145,8 @@
"name": "integrationUserPermission_integration_id_integration_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1279,23 +1154,15 @@
"name": "integrationUserPermission_user_id_user_id_fk",
"tableFrom": "integrationUserPermission",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationUserPermission_integration_id_user_id_permission_pk": {
"columns": [
"integration_id",
"user_id",
"permission"
],
"columns": ["integration_id", "user_id", "permission"],
"name": "integrationUserPermission_integration_id_user_id_permission_pk"
}
},
@@ -1344,9 +1211,7 @@
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": [
"kind"
],
"columns": ["kind"],
"isUnique": false
}
},
@@ -1355,12 +1220,8 @@
"name": "integration_app_id_app_id_fk",
"tableFrom": "integration",
"tableTo": "app",
"columnsFrom": [
"app_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["app_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1404,9 +1265,7 @@
"indexes": {
"invite_token_unique": {
"name": "invite_token_unique",
"columns": [
"token"
],
"columns": ["token"],
"isUnique": true
}
},
@@ -1415,12 +1274,8 @@
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1488,12 +1343,8 @@
"name": "item_layout_item_id_item_id_fk",
"tableFrom": "item_layout",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1501,12 +1352,8 @@
"name": "item_layout_section_id_section_id_fk",
"tableFrom": "item_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1514,23 +1361,15 @@
"name": "item_layout_layout_id_layout_id_fk",
"tableFrom": "item_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"item_layout_item_id_section_id_layout_id_pk": {
"columns": [
"item_id",
"section_id",
"layout_id"
],
"columns": ["item_id", "section_id", "layout_id"],
"name": "item_layout_item_id_section_id_layout_id_pk"
}
},
@@ -1584,12 +1423,8 @@
"name": "item_board_id_board_id_fk",
"tableFrom": "item",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1644,12 +1479,8 @@
"name": "layout_board_id_board_id_fk",
"tableFrom": "layout",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1718,12 +1549,8 @@
"name": "media_creator_id_user_id_fk",
"tableFrom": "media",
"tableTo": "user",
"columnsFrom": [
"creator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -1827,9 +1654,7 @@
"indexes": {
"search_engine_short_unique": {
"name": "search_engine_short_unique",
"columns": [
"short"
],
"columns": ["short"],
"isUnique": true
}
},
@@ -1838,12 +1663,8 @@
"name": "search_engine_integration_id_integration_id_fk",
"tableFrom": "search_engine",
"tableTo": "integration",
"columnsFrom": [
"integration_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -1884,12 +1705,8 @@
"name": "section_collapse_state_user_id_user_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1897,22 +1714,15 @@
"name": "section_collapse_state_section_id_section_id_fk",
"tableFrom": "section_collapse_state",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_collapse_state_user_id_section_id_pk": {
"columns": [
"user_id",
"section_id"
],
"columns": ["user_id", "section_id"],
"name": "section_collapse_state_user_id_section_id_pk"
}
},
@@ -1978,12 +1788,8 @@
"name": "section_layout_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -1991,12 +1797,8 @@
"name": "section_layout_layout_id_layout_id_fk",
"tableFrom": "section_layout",
"tableTo": "layout",
"columnsFrom": [
"layout_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["layout_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
@@ -2004,22 +1806,15 @@
"name": "section_layout_parent_section_id_section_id_fk",
"tableFrom": "section_layout",
"tableTo": "section",
"columnsFrom": [
"parent_section_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["parent_section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"section_layout_section_id_layout_id_pk": {
"columns": [
"section_id",
"layout_id"
],
"columns": ["section_id", "layout_id"],
"name": "section_layout_section_id_layout_id_pk"
}
},
@@ -2086,12 +1881,8 @@
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": [
"board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2122,9 +1913,7 @@
"indexes": {
"serverSetting_settingKey_unique": {
"name": "serverSetting_settingKey_unique",
"columns": [
"setting_key"
],
"columns": ["setting_key"],
"isUnique": true
}
},
@@ -2161,9 +1950,7 @@
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": [
"user_id"
],
"columns": ["user_id"],
"isUnique": false
}
},
@@ -2172,12 +1959,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -2215,10 +1998,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"columns": [
"hostname",
"thumbprint"
],
"columns": ["hostname", "thumbprint"],
"name": "trusted_certificate_hostname_hostname_thumbprint_pk"
}
},
@@ -2362,12 +2142,8 @@
"name": "user_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2375,12 +2151,8 @@
"name": "user_mobile_home_board_id_board_id_fk",
"tableFrom": "user",
"tableTo": "board",
"columnsFrom": [
"mobile_home_board_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["mobile_home_board_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
},
@@ -2388,12 +2160,8 @@
"name": "user_default_search_engine_id_search_engine_id_fk",
"tableFrom": "user",
"tableTo": "search_engine",
"columnsFrom": [
"default_search_engine_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["default_search_engine_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
@@ -2431,10 +2199,7 @@
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": [
"identifier",
"token"
],
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
@@ -2479,22 +2244,15 @@
"name": "widget_secret_item_id_item_id_fk",
"tableFrom": "widget_secret",
"tableTo": "item",
"columnsFrom": [
"item_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"widget_secret_item_id_kind_pk": {
"columns": [
"item_id",
"kind"
],
"columns": ["item_id", "kind"],
"name": "widget_secret_item_id_kind_pk"
}
},
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -295,6 +295,20 @@
"when": 1783786658943,
"tag": "0041_add_right_click_preference",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1784305283910,
"tag": "0042_custom_widget_v2_tables",
"breakpoints": true
},
{
"idx": 43,
"version": "6",
"when": 1786212459480,
"tag": "0043_numerous_forgotten_one",
"breakpoints": true
}
]
}
}
+2
View File
@@ -9,6 +9,7 @@
"exports": {
".": "./index.ts",
"./collection": "./collection.ts",
"./proxy-reader": "./proxy-reader.ts",
"./schema": "./schema/index.ts",
"./test": "./test/index.ts",
"./queries": "./queries/index.ts",
@@ -46,6 +47,7 @@
"@auth/core": "catalog:",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/custom-widgets": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
@@ -0,0 +1,128 @@
// @vitest-environment node
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import Database from "better-sqlite3";
import { MySqlContainer } from "@testcontainers/mysql";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import mysql from "mysql2/promise";
import { Client } from "pg";
import { stringify } from "superjson";
import { describe, expect, test, vi } from "vitest";
const mocks = vi.hoisted(() => ({
dbEnv: {} as Record<string, unknown>,
}));
vi.mock("@homarr/core/infrastructure/db/env", () => ({ dbEnv: mocks.dbEnv }));
const configureDriver = (values: Record<string, unknown>) => {
vi.resetModules();
for (const key of Object.keys(mocks.dbEnv)) delete mocks.dbEnv[key];
Object.assign(mocks.dbEnv, values);
};
const readSettingsAsync = async () => {
const reader = await import("./proxy-reader");
const result = {
locale: await reader.getDefaultLocaleForProxyAsync(),
onboardingStep: await reader.getOnboardingStepForProxyAsync(),
};
await reader.closeProxySettingsReaderAsync();
return result;
};
describe("proxy settings reader database compatibility", () => {
test("reads SQLite settings and falls back for malformed culture data", async () => {
const databasePath = path.join(os.tmpdir(), `homarr-proxy-reader-${process.pid}-${Date.now()}.sqlite`);
const database = new Database(databasePath);
database.exec("CREATE TABLE onboarding (step TEXT NOT NULL)");
database.exec('CREATE TABLE "serverSetting" (setting_key TEXT NOT NULL, value TEXT NOT NULL)');
database.prepare("INSERT INTO onboarding (step) VALUES (?)").run("finish");
database
.prepare('INSERT INTO "serverSetting" (setting_key, value) VALUES (?, ?)')
.run("culture", stringify({ defaultLocale: "de" }));
database.close();
try {
configureDriver({ DRIVER: "better-sqlite3", URL: databasePath });
await expect(readSettingsAsync()).resolves.toEqual({ locale: "de", onboardingStep: "finish" });
const malformedDatabase = new Database(databasePath);
malformedDatabase.prepare('UPDATE "serverSetting" SET value = ? WHERE setting_key = ?').run("{", "culture");
malformedDatabase.close();
vi.resetModules();
await expect(readSettingsAsync()).resolves.toEqual({ locale: "en", onboardingStep: "finish" });
} finally {
await fs.rm(databasePath, { force: true });
}
});
test("reads MySQL settings through the bounded host configuration", async () => {
const container = await new MySqlContainer("mysql:latest").start();
try {
const connection = await mysql.createConnection({
host: container.getHost(),
port: container.getPort(),
database: container.getDatabase(),
user: container.getUsername(),
password: container.getUserPassword(),
});
await connection.execute("CREATE TABLE onboarding (step VARCHAR(32) NOT NULL)");
await connection.execute("CREATE TABLE serverSetting (setting_key VARCHAR(64) NOT NULL, value TEXT NOT NULL)");
await connection.execute("INSERT INTO onboarding (step) VALUES (?)", ["finish"]);
await connection.execute("INSERT INTO serverSetting (setting_key, value) VALUES (?, ?)", [
"culture",
stringify({ defaultLocale: "fr" }),
]);
await connection.end();
configureDriver({
DRIVER: "mysql2",
HOST: container.getHost(),
PORT: container.getPort(),
NAME: container.getDatabase(),
USER: container.getUsername(),
PASSWORD: container.getUserPassword(),
});
await expect(readSettingsAsync()).resolves.toEqual({ locale: "fr", onboardingStep: "finish" });
} finally {
await container.stop();
}
}, 120_000);
test("reads PostgreSQL settings through the bounded host configuration", async () => {
const container = await new PostgreSqlContainer("postgres:latest").start();
try {
const connection = new Client({
host: container.getHost(),
port: container.getPort(),
database: container.getDatabase(),
user: container.getUsername(),
password: container.getPassword(),
});
await connection.connect();
await connection.query("CREATE TABLE onboarding (step TEXT NOT NULL)");
await connection.query('CREATE TABLE "serverSetting" (setting_key TEXT NOT NULL, value TEXT NOT NULL)');
await connection.query("INSERT INTO onboarding (step) VALUES ($1)", ["finish"]);
await connection.query('INSERT INTO "serverSetting" (setting_key, value) VALUES ($1, $2)', [
"culture",
stringify({ defaultLocale: "es" }),
]);
await connection.end();
configureDriver({
DRIVER: "node-postgres",
HOST: container.getHost(),
PORT: container.getPort(),
NAME: container.getDatabase(),
USER: container.getUsername(),
PASSWORD: container.getPassword(),
});
await expect(readSettingsAsync()).resolves.toEqual({ locale: "es", onboardingStep: "finish" });
} finally {
await container.stop();
}
}, 120_000);
});
+128
View File
@@ -0,0 +1,128 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const query = {
where: vi.fn(),
limit: vi.fn().mockResolvedValue([{ step: "finish" }]),
};
query.where.mockReturnValue(query);
return {
dbEnv: {
DRIVER: "mysql2",
URL: "mysql://homarr:test@localhost:3306/homarr",
} as Record<string, unknown>,
mysqlCreatePool: vi.fn(() => ({})),
postgresPool: vi.fn(function PostgresPool() {
return {};
}),
database: {
select: vi.fn(() => ({ from: vi.fn(() => query) })),
},
};
});
vi.mock("@homarr/core/infrastructure/db/env", () => ({ dbEnv: mocks.dbEnv }));
vi.mock("mysql2", () => ({ default: { createPool: mocks.mysqlCreatePool } }));
vi.mock("pg", () => ({ Pool: mocks.postgresPool }));
vi.mock("drizzle-orm/mysql2", () => ({ drizzle: vi.fn(() => mocks.database) }));
vi.mock("drizzle-orm/node-postgres", () => ({ drizzle: vi.fn(() => mocks.database) }));
vi.mock("./proxy/mysql", () => ({
proxySchema: { onboarding: { step: {} }, serverSettings: { settingKey: {}, value: {} } },
}));
vi.mock("./proxy/postgresql", () => ({
proxySchema: { onboarding: { step: {} }, serverSettings: { settingKey: {}, value: {} } },
}));
const initializeProxyReader = async () => {
const { getOnboardingStepForProxyAsync } = await import("./proxy-reader");
await getOnboardingStepForProxyAsync();
};
describe("proxy database pools", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
for (const key of Object.keys(mocks.dbEnv)) delete mocks.dbEnv[key];
mocks.mysqlCreatePool.mockReturnValue({});
});
it("bounds MySQL URL pools instead of falling back to the driver defaults", async () => {
Object.assign(mocks.dbEnv, {
DRIVER: "mysql2",
URL: "mysql://homarr:test@localhost:3306/homarr",
});
await initializeProxyReader();
expect(mocks.mysqlCreatePool).toHaveBeenCalledWith({
uri: "mysql://homarr:test@localhost:3306/homarr",
connectionLimit: 1,
maxIdle: 1,
idleTimeout: 60_000,
enableKeepAlive: true,
});
});
it("bounds MySQL host pools instead of falling back to the driver defaults", async () => {
Object.assign(mocks.dbEnv, {
DRIVER: "mysql2",
HOST: "localhost",
PORT: 3306,
NAME: "homarr",
USER: "homarr",
PASSWORD: "test",
});
await initializeProxyReader();
expect(mocks.mysqlCreatePool).toHaveBeenCalledWith(expect.objectContaining({ connectionLimit: 1, maxIdle: 1 }));
});
it("bounds PostgreSQL URL pools instead of falling back to the driver defaults", async () => {
Object.assign(mocks.dbEnv, {
DRIVER: "node-postgres",
URL: "postgresql://homarr:test@localhost:5432/homarr",
});
await initializeProxyReader();
expect(mocks.postgresPool).toHaveBeenCalledWith({
connectionString: "postgresql://homarr:test@localhost:5432/homarr",
max: 1,
idleTimeoutMillis: 60_000,
allowExitOnIdle: false,
});
});
it("bounds PostgreSQL host pools instead of falling back to the driver defaults", async () => {
Object.assign(mocks.dbEnv, {
DRIVER: "node-postgres",
HOST: "localhost",
PORT: 5432,
NAME: "homarr",
USER: "homarr",
PASSWORD: "test",
});
await initializeProxyReader();
expect(mocks.postgresPool).toHaveBeenCalledWith(expect.objectContaining({ max: 1 }));
});
it("retries initialization after a transient driver failure", async () => {
Object.assign(mocks.dbEnv, {
DRIVER: "mysql2",
URL: "mysql://homarr:test@localhost:3306/homarr",
});
mocks.mysqlCreatePool.mockImplementationOnce(() => {
throw new Error("database unavailable");
});
const { getOnboardingStepForProxyAsync } = await import("./proxy-reader");
await expect(getOnboardingStepForProxyAsync()).rejects.toThrow("database unavailable");
await expect(getOnboardingStepForProxyAsync()).resolves.toBe("finish");
expect(mocks.mysqlCreatePool).toHaveBeenCalledTimes(2);
});
});
+173
View File
@@ -0,0 +1,173 @@
import { parse as parseSuperJson } from "superjson";
import { eq } from "drizzle-orm";
import type { SQLWrapper } from "drizzle-orm";
import { defaultServerSettings } from "@homarr/server-settings";
import { dbEnv } from "@homarr/core/infrastructure/db/env";
type ProxyRow = Record<string, unknown>;
type ProxySchema = {
onboarding: { step: SQLWrapper };
serverSettings: { settingKey: SQLWrapper; value: SQLWrapper };
};
type ProxyQuery = {
where(condition: unknown): ProxyQuery;
limit(count: number): Promise<ProxyRow[]>;
};
type ProxyDatabase = {
select(selection: Record<string, unknown>): { from(table: unknown): ProxyQuery };
};
type ProxyReader = {
closeAsync: () => Promise<void>;
getOnboardingStepAsync: () => Promise<string>;
getDefaultLocaleAsync: () => Promise<string>;
};
// The proxy is long-lived and only performs two single-row reads. Keep its
// independent pool deliberately smaller than the application pool.
const proxyPoolConnectionLimit = 1;
const createProxyReader = (database: unknown, schema: ProxySchema, closeAsync: () => Promise<void>): ProxyReader => {
const queryDatabase = database as ProxyDatabase;
return {
closeAsync,
async getOnboardingStepAsync() {
const rows = await queryDatabase.select({ step: schema.onboarding.step }).from(schema.onboarding).limit(1);
const step = rows[0]?.step;
return typeof step === "string" ? step : "start";
},
async getDefaultLocaleAsync() {
const rows = await queryDatabase
.select({ value: schema.serverSettings.value })
.from(schema.serverSettings)
.where(eq(schema.serverSettings.settingKey, "culture"))
.limit(1);
const value = rows[0]?.value;
if (typeof value !== "string") return defaultServerSettings.culture.defaultLocale;
try {
const culture: unknown = parseSuperJson(value);
if (
typeof culture === "object" &&
culture !== null &&
"defaultLocale" in culture &&
typeof culture.defaultLocale === "string"
) {
return culture.defaultLocale;
}
} catch {
// Fall through to the default when a legacy or malformed value cannot be decoded.
}
return defaultServerSettings.culture.defaultLocale;
},
};
};
const createProxyReaderAsync = async (): Promise<ProxyReader> => {
switch (dbEnv.DRIVER) {
case "better-sqlite3": {
const [{ default: Database }, { drizzle }, { proxySchema }] = await Promise.all([
import("better-sqlite3"),
import("drizzle-orm/better-sqlite3"),
import("./proxy/sqlite"),
]);
const connection = new Database(dbEnv.URL);
const database = drizzle(connection, { schema: proxySchema });
return createProxyReader(database, proxySchema, async () => {
connection.close();
});
}
case "mysql2": {
const [{ default: mysql }, { drizzle }, { proxySchema }] = await Promise.all([
import("mysql2"),
import("drizzle-orm/mysql2"),
import("./proxy/mysql"),
]);
const connection = dbEnv.HOST
? mysql.createPool({
host: dbEnv.HOST,
port: dbEnv.PORT,
database: dbEnv.NAME,
user: dbEnv.USER,
password: dbEnv.PASSWORD,
connectionLimit: proxyPoolConnectionLimit,
maxIdle: 1,
idleTimeout: 60_000,
enableKeepAlive: true,
})
: mysql.createPool({
uri: dbEnv.URL,
connectionLimit: proxyPoolConnectionLimit,
maxIdle: 1,
idleTimeout: 60_000,
enableKeepAlive: true,
});
const database = drizzle(connection, { schema: proxySchema, mode: "default" });
return createProxyReader(
database,
proxySchema,
async () =>
await new Promise<void>((resolve, reject) => {
connection.end((error) => {
if (error) reject(error);
else resolve();
});
}),
);
}
case "node-postgres": {
const [{ Pool }, { drizzle }, { proxySchema }] = await Promise.all([
import("pg"),
import("drizzle-orm/node-postgres"),
import("./proxy/postgresql"),
]);
const connection = dbEnv.HOST
? new Pool({
host: dbEnv.HOST,
port: dbEnv.PORT,
database: dbEnv.NAME,
user: dbEnv.USER,
password: dbEnv.PASSWORD,
max: proxyPoolConnectionLimit,
idleTimeoutMillis: 60_000,
allowExitOnIdle: false,
})
: new Pool({
connectionString: dbEnv.URL,
max: proxyPoolConnectionLimit,
idleTimeoutMillis: 60_000,
allowExitOnIdle: false,
});
const database = drizzle({ client: connection, schema: proxySchema });
return createProxyReader(database, proxySchema, async () => {
await connection.end();
});
}
}
};
let proxyReaderPromise: Promise<ProxyReader> | null = null;
const getProxyReaderAsync = () => {
if (!proxyReaderPromise) {
proxyReaderPromise = createProxyReaderAsync().catch((error: unknown) => {
proxyReaderPromise = null;
throw error;
});
}
return proxyReaderPromise;
};
export const getOnboardingStepForProxyAsync = async () => (await getProxyReaderAsync()).getOnboardingStepAsync();
export const getDefaultLocaleForProxyAsync = async () => (await getProxyReaderAsync()).getDefaultLocaleAsync();
export const closeProxySettingsReaderAsync = async () => {
if (!proxyReaderPromise) return;
const reader = await proxyReaderPromise;
proxyReaderPromise = null;
await reader.closeAsync();
};
+11
View File
@@ -0,0 +1,11 @@
import { mysqlTable, text, varchar } from "drizzle-orm/mysql-core";
export const proxySchema = {
onboarding: mysqlTable("onboarding", {
step: varchar("step", { length: 32 }).notNull(),
}),
serverSettings: mysqlTable("serverSetting", {
settingKey: varchar("setting_key", { length: 64 }).notNull(),
value: text("value").notNull(),
}),
};
+11
View File
@@ -0,0 +1,11 @@
import { pgTable, text } from "drizzle-orm/pg-core";
export const proxySchema = {
onboarding: pgTable("onboarding", {
step: text("step").notNull(),
}),
serverSettings: pgTable("serverSetting", {
settingKey: text("setting_key").notNull(),
value: text("value").notNull(),
}),
};
+11
View File
@@ -0,0 +1,11 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const proxySchema = {
onboarding: sqliteTable("onboarding", {
step: text("step").notNull(),
}),
serverSettings: sqliteTable("serverSetting", {
settingKey: text("setting_key").notNull(),
value: text("value").notNull(),
}),
};
+4
View File
@@ -51,6 +51,8 @@ export const {
cronJobConfigurations,
customWidgetDefinitions,
customWidgetSecrets,
legacyCustomWidgetDefinitions,
legacyCustomWidgetSecrets,
widgetSecrets,
} = schema;
@@ -62,3 +64,5 @@ export type Integration = InferSelectModel<typeof schema.integrations>;
export type IntegrationSecret = InferSelectModel<typeof schema.integrationSecrets>;
export type CustomWidgetDefinition = InferSelectModel<typeof schema.customWidgetDefinitions>;
export type CustomWidgetSecret = InferSelectModel<typeof schema.customWidgetSecrets>;
export type LegacyCustomWidgetDefinition = InferSelectModel<typeof schema.legacyCustomWidgetDefinitions>;
export type LegacyCustomWidgetSecret = InferSelectModel<typeof schema.legacyCustomWidgetSecrets>;
+86 -22
View File
@@ -6,6 +6,7 @@ import type { AnyMySqlColumn } from "drizzle-orm/mysql-core";
import {
boolean,
customType,
foreignKey,
index,
int,
mysqlTable,
@@ -39,12 +40,7 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import type {
CustomWidgetAuthType,
CustomWidgetDisplayType,
CustomWidgetMethod,
CustomWidgetSecretKind,
} from "@homarr/validation/custom-widget";
import type { CustomWidgetSecretKind } from "@homarr/custom-widgets/core";
const customBlob = customType<{ data: Buffer }>({
dataType() {
@@ -211,8 +207,8 @@ export const integrations = mysqlTable(
kind: varchar({ length: 128 }).$type<IntegrationKind>().notNull(),
appId: varchar({ length: 128 }).references(() => apps.id, { onDelete: "set null" }),
},
(integrations) => ({
kindIdx: index("integration__kind_idx").on(integrations.kind),
(table) => ({
kindIdx: index("integration__kind_idx").on(table.kind),
}),
);
@@ -344,6 +340,8 @@ export const layouts = mysqlTable("layout", {
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
columnCount: tinyint().notNull(),
leftGutterColumnCount: tinyint().notNull().default(0),
rightGutterColumnCount: tinyint().notNull().default(0),
breakpoint: smallint().notNull().default(0),
});
@@ -505,18 +503,65 @@ export const onboarding = mysqlTable("onboarding", {
previousStep: varchar({ length: 64 }).$type<OnboardingStep>(),
});
export const customWidgetDefinitions = mysqlTable("custom_widget_definition", {
/**
* Read-only v1 definitions retained during the Custom JSX v2 upgrade.
* Their original physical table names keep the database readable by a v1 binary.
*/
export const legacyCustomWidgetDefinitions = mysqlTable(
"custom_widget_definition",
{
id: varchar({ length: 64 }).notNull().primaryKey(),
name: varchar({ length: 256 }).notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: varchar({ length: 32 }).notNull().default("none"),
headerName: varchar({ length: 256 }),
method: varchar({ length: 16 }).notNull().default("GET"),
requestBody: text(),
displayType: varchar({ length: 32 }).notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
enabled: boolean().notNull().default(true),
createdAt: timestamp({ mode: "date" }).notNull().defaultNow(),
updatedAt: timestamp({ mode: "date" }).notNull().defaultNow(),
creatorId: varchar({ length: 64 }),
},
(table) => ({
creatorFk: foreignKey({
columns: [table.creatorId],
foreignColumns: [users.id],
name: "custom_widget_definition_creator_id_user_id_fk",
}).onDelete("set null"),
}),
);
export const legacyCustomWidgetSecrets = mysqlTable(
"custom_widget_secret",
{
kind: varchar({ length: 64 }).$type<CustomWidgetSecretKind>().notNull(),
encryptedValue: text("value").$type<`${string}.${string}`>().notNull(),
updatedAt: timestamp().notNull(),
definitionId: varchar({ length: 64 }).notNull(),
},
(table) => ({
compoundKey: primaryKey({ columns: [table.definitionId, table.kind] }),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [legacyCustomWidgetDefinitions.id],
name: "cw_secret_definition_id_cw_definition_id_fk",
}).onDelete("cascade"),
}),
);
export const customWidgetDefinitions = mysqlTable("custom_widget_v2_definition", {
id: varchar({ length: 64 }).notNull().primaryKey(),
name: varchar({ length: 256 }).notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: varchar({ length: 32 }).$type<CustomWidgetAuthType>().notNull().default("none"),
headerName: varchar({ length: 256 }),
method: varchar({ length: 16 }).$type<CustomWidgetMethod>().notNull().default("GET"),
requestBody: text(),
displayType: varchar({ length: 32 }).$type<CustomWidgetDisplayType>().notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
sources: text().notNull(),
requests: text().notNull(),
options: text().notNull(),
template: text().notNull(),
enabled: boolean().notNull().default(true),
createdAt: timestamp({ mode: "date" }).notNull().defaultNow(),
updatedAt: timestamp({ mode: "date" }).notNull().defaultNow(),
@@ -524,21 +569,25 @@ export const customWidgetDefinitions = mysqlTable("custom_widget_definition", {
});
export const customWidgetSecrets = mysqlTable(
"custom_widget_secret",
"custom_widget_v2_secret",
{
sourceId: varchar({ length: 64 }).notNull(),
kind: varchar({ length: 64 }).$type<CustomWidgetSecretKind>().notNull(),
value: text().$type<`${string}.${string}`>().notNull(),
encryptedValue: text("encrypted_value").$type<`${string}.${string}`>().notNull(),
updatedAt: timestamp()
.$onUpdateFn(() => new Date())
.notNull(),
definitionId: varchar({ length: 64 })
.notNull()
.references(() => customWidgetDefinitions.id, { onDelete: "cascade" }),
definitionId: varchar({ length: 64 }).notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.definitionId, table.kind],
columns: [table.definitionId, table.sourceId, table.kind],
}),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [customWidgetDefinitions.id],
name: "custom_widget_v2_secret_definition_id_fk",
}).onDelete("cascade"),
}),
);
@@ -854,3 +903,18 @@ export const customWidgetSecretRelations = relations(customWidgetSecrets, ({ one
references: [customWidgetDefinitions.id],
}),
}));
export const legacyCustomWidgetDefinitionRelations = relations(legacyCustomWidgetDefinitions, ({ many, one }) => ({
secrets: many(legacyCustomWidgetSecrets),
creator: one(users, {
fields: [legacyCustomWidgetDefinitions.creatorId],
references: [users.id],
}),
}));
export const legacyCustomWidgetSecretRelations = relations(legacyCustomWidgetSecrets, ({ one }) => ({
definition: one(legacyCustomWidgetDefinitions, {
fields: [legacyCustomWidgetSecrets.definitionId],
references: [legacyCustomWidgetDefinitions.id],
}),
}));
+86 -22
View File
@@ -6,6 +6,7 @@ import type { AnyPgColumn } from "drizzle-orm/pg-core";
import {
boolean,
customType,
foreignKey,
index,
integer,
pgTable,
@@ -38,12 +39,7 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import type {
CustomWidgetAuthType,
CustomWidgetDisplayType,
CustomWidgetMethod,
CustomWidgetSecretKind,
} from "@homarr/validation/custom-widget";
import type { CustomWidgetSecretKind } from "@homarr/custom-widgets/core";
const customBlob = customType<{ data: Buffer }>({
dataType() {
@@ -210,8 +206,8 @@ export const integrations = pgTable(
kind: varchar({ length: 128 }).$type<IntegrationKind>().notNull(),
appId: varchar({ length: 128 }).references(() => apps.id, { onDelete: "set null" }),
},
(integrations) => ({
kindIdx: index("integration__kind_idx").on(integrations.kind),
(table) => ({
kindIdx: index("integration__kind_idx").on(table.kind),
}),
);
@@ -343,6 +339,8 @@ export const layouts = pgTable("layout", {
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
columnCount: smallint().notNull(),
leftGutterColumnCount: smallint().notNull().default(0),
rightGutterColumnCount: smallint().notNull().default(0),
breakpoint: smallint().notNull().default(0),
});
@@ -504,18 +502,65 @@ export const onboarding = pgTable("onboarding", {
previousStep: varchar({ length: 64 }).$type<OnboardingStep>(),
});
export const customWidgetDefinitions = pgTable("custom_widget_definition", {
/**
* Read-only v1 definitions retained during the Custom JSX v2 upgrade.
* Their original physical table names keep the database readable by a v1 binary.
*/
export const legacyCustomWidgetDefinitions = pgTable(
"custom_widget_definition",
{
id: varchar({ length: 64 }).notNull().primaryKey(),
name: varchar({ length: 256 }).notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: varchar({ length: 32 }).notNull().default("none"),
headerName: varchar({ length: 256 }),
method: varchar({ length: 16 }).notNull().default("GET"),
requestBody: text(),
displayType: varchar({ length: 32 }).notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
enabled: boolean().notNull().default(true),
createdAt: timestamp({ mode: "date" }).notNull().defaultNow(),
updatedAt: timestamp({ mode: "date" }).notNull().defaultNow(),
creatorId: varchar({ length: 64 }),
},
(table) => ({
creatorFk: foreignKey({
columns: [table.creatorId],
foreignColumns: [users.id],
name: "custom_widget_definition_creator_id_user_id_fk",
}).onDelete("set null"),
}),
);
export const legacyCustomWidgetSecrets = pgTable(
"custom_widget_secret",
{
kind: varchar({ length: 64 }).$type<CustomWidgetSecretKind>().notNull(),
encryptedValue: text("value").$type<`${string}.${string}`>().notNull(),
updatedAt: timestamp().notNull(),
definitionId: varchar({ length: 64 }).notNull(),
},
(table) => ({
compoundKey: primaryKey({ columns: [table.definitionId, table.kind] }),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [legacyCustomWidgetDefinitions.id],
name: "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
}).onDelete("cascade"),
}),
);
export const customWidgetDefinitions = pgTable("custom_widget_v2_definition", {
id: varchar({ length: 64 }).notNull().primaryKey(),
name: varchar({ length: 256 }).notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: varchar({ length: 32 }).$type<CustomWidgetAuthType>().notNull().default("none"),
headerName: varchar({ length: 256 }),
method: varchar({ length: 16 }).$type<CustomWidgetMethod>().notNull().default("GET"),
requestBody: text(),
displayType: varchar({ length: 32 }).$type<CustomWidgetDisplayType>().notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
sources: text().notNull(),
requests: text().notNull(),
options: text().notNull(),
template: text().notNull(),
enabled: boolean().notNull().default(true),
createdAt: timestamp({ mode: "date" }).notNull().defaultNow(),
updatedAt: timestamp({ mode: "date" }).notNull().defaultNow(),
@@ -523,21 +568,25 @@ export const customWidgetDefinitions = pgTable("custom_widget_definition", {
});
export const customWidgetSecrets = pgTable(
"custom_widget_secret",
"custom_widget_v2_secret",
{
sourceId: varchar({ length: 64 }).notNull(),
kind: varchar({ length: 64 }).$type<CustomWidgetSecretKind>().notNull(),
value: text().$type<`${string}.${string}`>().notNull(),
encryptedValue: text("encrypted_value").$type<`${string}.${string}`>().notNull(),
updatedAt: timestamp()
.$onUpdateFn(() => new Date())
.notNull(),
definitionId: varchar({ length: 64 })
.notNull()
.references(() => customWidgetDefinitions.id, { onDelete: "cascade" }),
definitionId: varchar({ length: 64 }).notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.definitionId, table.kind],
columns: [table.definitionId, table.sourceId, table.kind],
}),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [customWidgetDefinitions.id],
name: "custom_widget_v2_secret_definition_id_fk",
}).onDelete("cascade"),
}),
);
@@ -853,3 +902,18 @@ export const customWidgetSecretRelations = relations(customWidgetSecrets, ({ one
references: [customWidgetDefinitions.id],
}),
}));
export const legacyCustomWidgetDefinitionRelations = relations(legacyCustomWidgetDefinitions, ({ many, one }) => ({
secrets: many(legacyCustomWidgetSecrets),
creator: one(users, {
fields: [legacyCustomWidgetDefinitions.creatorId],
references: [users.id],
}),
}));
export const legacyCustomWidgetSecretRelations = relations(legacyCustomWidgetSecrets, ({ one }) => ({
definition: one(legacyCustomWidgetDefinitions, {
fields: [legacyCustomWidgetSecrets.definitionId],
references: [legacyCustomWidgetDefinitions.id],
}),
}));
+90 -23
View File
@@ -3,7 +3,7 @@ import type { MantineSize } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import { relations, sql } from "drizzle-orm";
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { blob, foreignKey, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
import {
backgroundImageAttachments,
@@ -27,12 +27,7 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import type {
CustomWidgetAuthType,
CustomWidgetDisplayType,
CustomWidgetMethod,
CustomWidgetSecretKind,
} from "@homarr/validation/custom-widget";
import type { CustomWidgetSecretKind } from "@homarr/custom-widgets/core";
export * from "@homarr/core/infrastructure/certificates/hostnames/db/sqlite";
@@ -197,8 +192,8 @@ export const integrations = sqliteTable(
kind: text().$type<IntegrationKind>().notNull(),
appId: text().references(() => apps.id, { onDelete: "set null" }),
},
(integrations) => ({
kindIdx: index("integration__kind_idx").on(integrations.kind),
(table) => ({
kindIdx: index("integration__kind_idx").on(table.kind),
}),
);
@@ -329,6 +324,8 @@ export const layouts = sqliteTable("layout", {
.notNull()
.references(() => boards.id, { onDelete: "cascade" }),
columnCount: int().notNull(),
leftGutterColumnCount: int().notNull().default(0),
rightGutterColumnCount: int().notNull().default(0),
breakpoint: int().notNull().default(0),
});
@@ -490,18 +487,69 @@ export const onboarding = sqliteTable("onboarding", {
previousStep: text().$type<OnboardingStep>(),
});
export const customWidgetDefinitions = sqliteTable("custom_widget_definition", {
/**
* Read-only v1 definitions retained during the Custom JSX v2 upgrade.
* Their original physical table names keep the database readable by a v1 binary.
*/
export const legacyCustomWidgetDefinitions = sqliteTable(
"custom_widget_definition",
{
id: text().notNull().primaryKey(),
name: text().notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: text().notNull().default("none"),
headerName: text(),
method: text().notNull().default("GET"),
requestBody: text(),
displayType: text().notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
enabled: int({ mode: "boolean" }).notNull().default(true),
createdAt: int({ mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
updatedAt: int({ mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
creatorId: text(),
},
(table) => ({
creatorFk: foreignKey({
columns: [table.creatorId],
foreignColumns: [users.id],
name: "custom_widget_definition_creator_id_user_id_fk",
}).onDelete("set null"),
}),
);
export const legacyCustomWidgetSecrets = sqliteTable(
"custom_widget_secret",
{
kind: text().$type<CustomWidgetSecretKind>().notNull(),
encryptedValue: text("value").$type<`${string}.${string}`>().notNull(),
updatedAt: int({ mode: "timestamp" }).notNull(),
definitionId: text().notNull(),
},
(table) => ({
compoundKey: primaryKey({ columns: [table.definitionId, table.kind] }),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [legacyCustomWidgetDefinitions.id],
name: "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
}).onDelete("cascade"),
}),
);
export const customWidgetDefinitions = sqliteTable("custom_widget_v2_definition", {
id: text().notNull().primaryKey(),
name: text().notNull(),
description: text(),
iconUrl: text(),
url: text().notNull(),
authType: text().$type<CustomWidgetAuthType>().notNull().default("none"),
headerName: text(),
method: text().$type<CustomWidgetMethod>().notNull().default("GET"),
requestBody: text(),
displayType: text().$type<CustomWidgetDisplayType>().notNull().default("singleValue"),
displayConfig: text().default(emptySuperJSON).notNull(),
sources: text().notNull(),
requests: text().notNull(),
options: text().notNull(),
template: text().notNull(),
enabled: int({ mode: "boolean" }).notNull().default(true),
createdAt: int({ mode: "timestamp" })
.notNull()
@@ -513,21 +561,25 @@ export const customWidgetDefinitions = sqliteTable("custom_widget_definition", {
});
export const customWidgetSecrets = sqliteTable(
"custom_widget_secret",
"custom_widget_v2_secret",
{
sourceId: text().notNull(),
kind: text().$type<CustomWidgetSecretKind>().notNull(),
value: text().$type<`${string}.${string}`>().notNull(),
encryptedValue: text("encrypted_value").$type<`${string}.${string}`>().notNull(),
updatedAt: int({ mode: "timestamp" })
.$onUpdateFn(() => new Date())
.notNull(),
definitionId: text()
.notNull()
.references(() => customWidgetDefinitions.id, { onDelete: "cascade" }),
definitionId: text().notNull(),
},
(table) => ({
compoundKey: primaryKey({
columns: [table.definitionId, table.kind],
columns: [table.definitionId, table.sourceId, table.kind],
}),
definitionFk: foreignKey({
columns: [table.definitionId],
foreignColumns: [customWidgetDefinitions.id],
name: "custom_widget_v2_secret_definition_id_fk",
}).onDelete("cascade"),
}),
);
@@ -843,3 +895,18 @@ export const customWidgetSecretRelations = relations(customWidgetSecrets, ({ one
references: [customWidgetDefinitions.id],
}),
}));
export const legacyCustomWidgetDefinitionRelations = relations(legacyCustomWidgetDefinitions, ({ many, one }) => ({
secrets: many(legacyCustomWidgetSecrets),
creator: one(users, {
fields: [legacyCustomWidgetDefinitions.creatorId],
references: [users.id],
}),
}));
export const legacyCustomWidgetSecretRelations = relations(legacyCustomWidgetSecrets, ({ one }) => ({
definition: one(legacyCustomWidgetDefinitions, {
fields: [legacyCustomWidgetSecrets.definitionId],
references: [legacyCustomWidgetDefinitions.id],
}),
}));
@@ -1,18 +0,0 @@
{
"$schema": "homarr-custom-widget-v2",
"name": "Currency Exchange (JPY)",
"description": "Converts 50 Japanese Yen to EUR and USD using European Central Bank rates",
"url": "https://api.frankfurter.dev/v1/latest?from=JPY&to=EUR,USD&amount=50",
"authType": "none",
"method": "GET",
"displayType": "keyValue",
"displayConfig": {
"type": "keyValue",
"mappings": [
{ "label": "50 JPY → EUR", "jsonPath": "$.rates.EUR", "unit": "€" },
{ "label": "50 JPY → USD", "jsonPath": "$.rates.USD", "unit": "$" }
],
"layout": "list",
"columns": 2
}
}
@@ -1,17 +0,0 @@
{
"$schema": "homarr-custom-widget-v2",
"name": "Random Dog Fact",
"description": "Displays a random fun fact about dogs",
"url": "https://dogapi.dog/api/v2/facts",
"authType": "none",
"method": "GET",
"displayType": "singleValue",
"displayConfig": {
"type": "singleValue",
"jsonPath": "$.data[0].attributes.body",
"label": "Dog Fact",
"unit": "",
"valueSize": "sm",
"labelPosition": "above"
}
}
@@ -1,23 +0,0 @@
{
"$schema": "homarr-custom-widget-v2",
"name": "Jellyfin library",
"description": "Counts the number of movies, series, episodes and songs in the library",
"iconUrl": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
"url": "https://jellyfin.homelab.com/Items/Counts",
"authType": "apiKeyHeader",
"headerName": "X-Emby-Token",
"method": "GET",
"requestBody": null,
"displayType": "countGrid",
"displayConfig": {
"type": "countGrid",
"items": [
{ "label": "Movies", "jsonPath": "$.MovieCount", "unit": "" },
{ "label": "Series", "jsonPath": "$.SeriesCount", "unit": "" },
{ "label": "Episodes", "jsonPath": "$.EpisodeCount", "unit": "" },
{ "label": "Songs", "jsonPath": "$.SongCount", "unit": "" }
],
"columns": 4,
"valueSize": "lg"
}
}
@@ -1,14 +0,0 @@
{
"$schema": "homarr-custom-widget-v2",
"name": "Pokédex — Charizard",
"description": "Detailed Pokémon card with types, stats, tabs, pagination, and collapsible info",
"iconUrl": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png",
"url": "https://pokeapi.co/api/v2/pokemon/charizard",
"authType": "none",
"method": "GET",
"displayType": "customJsx",
"displayConfig": {
"type": "customJsx",
"template": "<Stack gap=\"sm\" p=\"xs\">\n <Group wrap=\"nowrap\">\n <Avatar\n src={data.sprites.front_default}\n size={80}\n radius=\"sm\"\n />\n <Stack gap={4} style={{flex: 1}}>\n <Title order={3} tt=\"capitalize\">{data.name}</Title>\n <Text size=\"xs\" c=\"dimmed\">#{String(data.id).padStart(3, \"0\")} · {data.height / 10} m · {data.weight / 10} kg</Text>\n <Group gap=\"xs\">\n {data.types.map(t =>\n <TypeBadge type={t.type.name} />\n )}\n </Group>\n </Stack>\n </Group>\n <TabsContainer defaultTab=\"stats\">\n <TabPanel value=\"stats\" label=\"Stats\">\n <Stack gap=\"xs\" pt=\"xs\">\n {data.stats.map(s =>\n <StatBar label={s.stat.name} value={s.base_stat} max={255} color={s.base_stat > 100 ? \"green\" : s.base_stat > 60 ? \"yellow\" : \"red\"} />\n )}\n </Stack>\n </TabPanel>\n <TabPanel value=\"abilities\" label=\"Abilities\">\n <Collapsible title=\"Abilities\" defaultOpen={true}>\n <Group gap=\"xs\" pt=\"xs\">\n {data.abilities.map(a =>\n <Badge variant={a.is_hidden ? \"outline\" : \"filled\"} color={a.is_hidden ? \"gray\" : \"blue\"} tt=\"capitalize\">\n {a.ability.name}{a.is_hidden ? \" (hidden)\" : \"\"}\n </Badge>\n )}\n </Group>\n </Collapsible>\n </TabPanel>\n <TabPanel value=\"moves\" label=\"Moves\">\n <PaginatedList pageSize={12}>\n {data.moves.map(m =>\n <Badge size=\"xs\" variant=\"light\" tt=\"capitalize\" mb={4}>{m.move.name}</Badge>\n )}\n </PaginatedList>\n </TabPanel>\n </TabsContainer>\n</Stack>"
}
}
@@ -0,0 +1,65 @@
import { parse } from "superjson";
import { expect } from "vitest";
import { BUNDLED_CUSTOM_WIDGETS, customWidgetDefinitionSchema } from "@homarr/custom-widgets/core";
import type { Database } from "..";
import { eq } from "..";
import { seedDataAsync } from "../migrations/seed";
import { customWidgetDefinitions, customWidgetSecrets } from "../schema";
const expectedSeedIds = BUNDLED_CUSTOM_WIDGETS.map(({ id }) => id).toSorted();
const reconstructWidget = (definition: typeof customWidgetDefinitions.$inferSelect) =>
customWidgetDefinitionSchema.parse({
$schema: "homarr-custom-widget-v2",
name: definition.name,
...(definition.description === null ? {} : { description: definition.description }),
...(definition.iconUrl === null ? {} : { iconUrl: definition.iconUrl }),
sources: parse(definition.sources),
requests: parse(definition.requests),
options: parse(definition.options),
template: definition.template,
});
export const expectBundledCustomWidgetsSeeded = async (db: Database) => {
const definitions = await db.query.customWidgetDefinitions.findMany({
where: (table, { like }) => like(table.id, "seed-%"),
});
expect(definitions.map(({ id }) => id).toSorted()).toEqual(expectedSeedIds);
expect(definitions.every(({ enabled }) => !enabled)).toBe(true);
expect(definitions.every(({ creatorId }) => creatorId === null)).toBe(true);
expect(await db.$count(customWidgetSecrets)).toBe(0);
for (const definition of definitions) {
const expected = BUNDLED_CUSTOM_WIDGETS.find(({ id }) => id === definition.id);
expect(expected).toBeDefined();
expect(reconstructWidget(definition)).toEqual(customWidgetDefinitionSchema.parse(expected?.widget));
}
await db
.update(customWidgetDefinitions)
.set({ name: "User-edited dog facts", enabled: true })
.where(eq(customWidgetDefinitions.id, "seed-dog-facts"));
await db.delete(customWidgetDefinitions).where(eq(customWidgetDefinitions.id, "seed-currency-exchange"));
await seedDataAsync(db);
const reseededDefinitions = await db.query.customWidgetDefinitions.findMany({
where: (table, { like }) => like(table.id, "seed-%"),
});
const editedDefinition = reseededDefinitions.find(({ id }) => id === "seed-dog-facts");
const reseededCurrency = reseededDefinitions.find(({ id }) => id === "seed-currency-exchange");
expect(reseededDefinitions.map(({ id }) => id).toSorted()).toEqual(expectedSeedIds);
expect(editedDefinition).toMatchObject({ name: "User-edited dog facts", enabled: true, creatorId: null });
expect(reseededDefinitions.filter(({ id }) => id !== "seed-dog-facts").every(({ enabled }) => !enabled)).toBe(true);
if (!reseededCurrency) throw new Error("Currency Exchange seed was not restored");
expect(reconstructWidget(reseededCurrency)).toEqual(
customWidgetDefinitionSchema.parse(
BUNDLED_CUSTOM_WIDGETS.find(({ id }) => id === "seed-currency-exchange")?.widget,
),
);
expect(await db.$count(customWidgetSecrets)).toBe(0);
};
+162 -1
View File
@@ -1,13 +1,28 @@
import path from "path";
import { readFileSync } from "node:fs";
import { MySqlContainer } from "@testcontainers/mysql";
import { drizzle } from "drizzle-orm/mysql2";
import { migrate } from "drizzle-orm/mysql2/migrator";
import mysql from "mysql2";
import { describe, test } from "vitest";
import SuperJSON from "superjson";
import { describe, expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import * as mysqlSchema from "../schema/mysql";
import type { Database } from "..";
import { seedDataAsync } from "../migrations/seed";
import { expectBundledCustomWidgetsSeeded } from "./custom-widget-seed-assertions";
const applyMigration = async (connection: mysql.Connection, fileName: string) => {
const migration = readFileSync(path.join(__dirname, "..", "migrations", "mysql", fileName), "utf8");
for (const statement of migration
.split("--> statement-breakpoint")
.map((value) => value.trim())
.filter(Boolean)) {
await connection.promise().query(statement);
}
};
describe("Mysql Migration", () => {
test("should add all tables and keys specified in migration files", async () => {
@@ -31,11 +46,157 @@ describe("Mysql Migration", () => {
await migrate(database, {
migrationsFolder: path.join(__dirname, "..", "migrations", "mysql"),
});
await seedDataAsync(database as unknown as Database);
// Check if users table exists
await database.query.users.findMany();
await expectBundledCustomWidgetsSeeded(database as unknown as Database);
const [customWidgetTables] = await connection
.promise()
.query<mysql.RowDataPacket[]>("SHOW TABLES LIKE 'custom_widget%'");
expect(customWidgetTables).toHaveLength(4);
connection.end();
await mysqlContainer.stop();
}, 120_000);
test("Custom Widget v2 migration preserves v1 data and board references", async () => {
const mysqlContainer = await new MySqlContainer("mysql:latest").start();
const connection = mysql.createConnection({
host: mysqlContainer.getHost(),
database: mysqlContainer.getDatabase(),
port: mysqlContainer.getPort(),
user: mysqlContainer.getUsername(),
password: mysqlContainer.getUserPassword(),
multipleStatements: true,
});
const sql = connection.promise();
try {
const legacyPlacementOptions = SuperJSON.stringify({ definitionId: "legacy-weather", refreshInterval: 30 });
await sql.query(`
CREATE TABLE \`user\` (\`id\` varchar(64) NOT NULL PRIMARY KEY);
CREATE TABLE \`groupPermission\` (
\`group_id\` varchar(64) NOT NULL,
\`permission\` text NOT NULL
);
CREATE TABLE \`item\` (
\`id\` varchar(64) NOT NULL PRIMARY KEY,
\`kind\` varchar(64) NOT NULL,
\`options\` text NOT NULL
);
CREATE TABLE \`custom_widget_definition\` (
\`id\` varchar(64) NOT NULL PRIMARY KEY,
\`name\` varchar(256) NOT NULL,
\`description\` text,
\`icon_url\` text,
\`url\` text NOT NULL,
\`auth_type\` varchar(32) NOT NULL DEFAULT 'none',
\`header_name\` varchar(256),
\`method\` varchar(16) NOT NULL DEFAULT 'GET',
\`request_body\` text,
\`display_type\` varchar(32) NOT NULL DEFAULT 'singleValue',
\`display_config\` text NOT NULL,
\`enabled\` boolean NOT NULL DEFAULT true,
\`created_at\` timestamp NOT NULL,
\`updated_at\` timestamp NOT NULL,
\`creator_id\` varchar(64),
CONSTRAINT \`custom_widget_definition_creator_id_user_id_fk\`
FOREIGN KEY (\`creator_id\`) REFERENCES \`user\`(\`id\`) ON DELETE SET NULL
);
CREATE TABLE \`custom_widget_secret\` (
\`kind\` varchar(64) NOT NULL,
\`value\` text NOT NULL,
\`updated_at\` timestamp NOT NULL,
\`definition_id\` varchar(64) NOT NULL,
PRIMARY KEY (\`definition_id\`, \`kind\`),
CONSTRAINT \`cw_secret_definition_id_cw_definition_id_fk\`
FOREIGN KEY (\`definition_id\`) REFERENCES \`custom_widget_definition\`(\`id\`) ON DELETE CASCADE
);
INSERT INTO \`user\` (\`id\`) VALUES ('owner');
INSERT INTO \`custom_widget_definition\` (
\`id\`, \`name\`, \`url\`, \`auth_type\`, \`method\`, \`display_type\`, \`display_config\`,
\`enabled\`, \`created_at\`, \`updated_at\`, \`creator_id\`
) VALUES (
'legacy-weather', 'Weather', 'https://example.test/weather', 'bearer', 'GET', 'singleValue',
'{"type":"singleValue","jsonPath":"$.temperature"}', true,
'2023-11-14 00:00:00', '2023-11-14 00:00:01', 'owner'
);
INSERT INTO \`custom_widget_secret\` (\`kind\`, \`value\`, \`updated_at\`, \`definition_id\`)
VALUES ('apiKey', 'encrypted.value', '2023-11-14 00:00:01', 'legacy-weather');
INSERT INTO \`groupPermission\` (\`group_id\`, \`permission\`) VALUES
('editors', 'custom-widget-manage'),
('editors', 'custom-widget-secret-write'),
('editors', 'board-create');
`);
await sql.execute("INSERT INTO `item` (`id`, `kind`, `options`) VALUES ('weather-item', 'customApi', ?)", [
legacyPlacementOptions,
]);
await applyMigration(connection, "0044_custom_widget_v2_tables.sql");
await sql.query(`
INSERT INTO \`custom_widget_v2_definition\` (
\`id\`, \`name\`, \`sources\`, \`requests\`, \`options\`, \`template\`, \`enabled\`,
\`created_at\`, \`updated_at\`, \`creator_id\`
) VALUES (
'legacy-weather', 'Weather v2', '[]', '[]', '[]', '<Text>Weather</Text>', true,
'2023-11-14 00:00:02', '2023-11-14 00:00:03', 'owner'
);
INSERT INTO \`custom_widget_v2_secret\` (
\`source_id\`, \`kind\`, \`encrypted_value\`, \`updated_at\`, \`definition_id\`
) VALUES ('weather', 'apiKey', 'encrypted.v2-value', '2023-11-14 00:00:03', 'legacy-weather');
`);
const [definitions] = await sql.query<mysql.RowDataPacket[]>(
"SELECT id, name, url, enabled, creator_id FROM custom_widget_definition",
);
const [secrets] = await sql.query<mysql.RowDataPacket[]>(
"SELECT definition_id, kind, value FROM custom_widget_secret",
);
expect(definitions).toEqual([
expect.objectContaining({
id: "legacy-weather",
name: "Weather",
url: "https://example.test/weather",
enabled: 1,
creator_id: "owner",
}),
]);
expect(secrets).toEqual([
expect.objectContaining({ definition_id: "legacy-weather", kind: "apiKey", value: "encrypted.value" }),
]);
const [v2Definitions] = await sql.query<mysql.RowDataPacket[]>(
"SELECT id, name, template FROM custom_widget_v2_definition",
);
const [v2Secrets] = await sql.query<mysql.RowDataPacket[]>(
"SELECT definition_id, source_id, kind, encrypted_value FROM custom_widget_v2_secret",
);
expect(v2Definitions).toEqual([
expect.objectContaining({ id: "legacy-weather", name: "Weather v2", template: "<Text>Weather</Text>" }),
]);
expect(v2Secrets).toEqual([
expect.objectContaining({
definition_id: "legacy-weather",
source_id: "weather",
kind: "apiKey",
encrypted_value: "encrypted.v2-value",
}),
]);
const [items] = await sql.query<mysql.RowDataPacket[]>("SELECT options FROM item WHERE id = 'weather-item'");
expect(items).toEqual([{ options: legacyPlacementOptions }]);
const [permissions] = await sql.query<mysql.RowDataPacket[]>(
"SELECT permission FROM groupPermission ORDER BY permission",
);
expect(permissions).toEqual([{ permission: "board-create" }]);
const [v1Columns] = await sql.query<mysql.RowDataPacket[]>("SHOW COLUMNS FROM custom_widget_definition");
expect(v1Columns.map((column) => column.Field)).toEqual(
expect.arrayContaining(["id", "url", "auth_type", "display_type", "display_config"]),
);
expect(v1Columns.map((column) => column.Field)).not.toContain("sources");
const [legacyTables] = await sql.query<mysql.RowDataPacket[]>("SHOW TABLES LIKE 'legacy_custom_widget%'");
expect(legacyTables).toEqual([]);
} finally {
await sql.end();
await mysqlContainer.stop();
}
}, 120_000);
});
+162 -1
View File
@@ -1,13 +1,28 @@
import path from "path";
import { readFileSync } from "node:fs";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { Pool } from "pg";
import { describe, test } from "vitest";
import SuperJSON from "superjson";
import { describe, expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import * as pgSchema from "../schema/postgresql";
import type { Database } from "..";
import { seedDataAsync } from "../migrations/seed";
import { expectBundledCustomWidgetsSeeded } from "./custom-widget-seed-assertions";
const applyMigration = async (pool: Pool, fileName: string) => {
const migration = readFileSync(path.join(__dirname, "..", "migrations", "postgresql", fileName), "utf8");
for (const statement of migration
.split("--> statement-breakpoint")
.map((value) => value.trim())
.filter(Boolean)) {
await pool.query(statement);
}
};
describe("PostgreSql Migration", () => {
test("should add all tables and keys specified in migration files", async () => {
@@ -36,13 +51,159 @@ describe("PostgreSql Migration", () => {
await migrate(database, {
migrationsFolder: path.join(__dirname, "..", "migrations", "postgresql"),
});
await seedDataAsync(database as unknown as Database);
// Check if users table exists
await database.query.users.findMany();
await expectBundledCustomWidgetsSeeded(database as unknown as Database);
expect(
(
await pool.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE 'custom_widget%'",
)
).rows,
).toHaveLength(4);
// Close the pool to release resources
await pool.end();
// Stop the container
await postgreSqlContainer.stop();
}, 40_000);
test("Custom Widget v2 migration preserves v1 data and board references", async () => {
const postgreSqlContainer = await new PostgreSqlContainer("postgres:latest").start();
const pool = new Pool({
user: postgreSqlContainer.getUsername(),
database: postgreSqlContainer.getDatabase(),
password: postgreSqlContainer.getPassword(),
port: postgreSqlContainer.getPort(),
host: postgreSqlContainer.getHost(),
});
try {
const legacyPlacementOptions = SuperJSON.stringify({ definitionId: "legacy-weather", refreshInterval: 30 });
await pool.query(`
CREATE TABLE "user" ("id" varchar(64) PRIMARY KEY NOT NULL);
CREATE TABLE "groupPermission" (
"group_id" varchar(64) NOT NULL,
"permission" text NOT NULL
);
CREATE TABLE "item" (
"id" varchar(64) PRIMARY KEY NOT NULL,
"kind" varchar(64) NOT NULL,
"options" text NOT NULL
);
CREATE TABLE "custom_widget_definition" (
"id" varchar(64) PRIMARY KEY NOT NULL,
"name" varchar(256) NOT NULL,
"description" text,
"icon_url" text,
"url" text NOT NULL,
"auth_type" varchar(32) DEFAULT 'none' NOT NULL,
"header_name" varchar(256),
"method" varchar(16) DEFAULT 'GET' NOT NULL,
"request_body" text,
"display_type" varchar(32) DEFAULT 'singleValue' NOT NULL,
"display_config" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"creator_id" varchar(64),
CONSTRAINT "custom_widget_definition_creator_id_user_id_fk"
FOREIGN KEY ("creator_id") REFERENCES "user"("id") ON DELETE SET NULL
);
CREATE TABLE "custom_widget_secret" (
"kind" varchar(64) NOT NULL,
"value" text NOT NULL,
"updated_at" timestamp NOT NULL,
"definition_id" varchar(64) NOT NULL,
CONSTRAINT "custom_widget_secret_definition_id_kind_pk" PRIMARY KEY("definition_id", "kind"),
CONSTRAINT "custom_widget_secret_definition_id_custom_widget_definition_id_fk"
FOREIGN KEY ("definition_id") REFERENCES "custom_widget_definition"("id") ON DELETE CASCADE
);
INSERT INTO "user" ("id") VALUES ('owner');
INSERT INTO "custom_widget_definition" (
"id", "name", "url", "auth_type", "method", "display_type", "display_config", "enabled",
"created_at", "updated_at", "creator_id"
) VALUES (
'legacy-weather', 'Weather', 'https://example.test/weather', 'bearer', 'GET', 'singleValue',
'{"type":"singleValue","jsonPath":"$.temperature"}', true,
'2023-11-14 00:00:00', '2023-11-14 00:00:01', 'owner'
);
INSERT INTO "custom_widget_secret" ("kind", "value", "updated_at", "definition_id")
VALUES ('apiKey', 'encrypted.value', '2023-11-14 00:00:01', 'legacy-weather');
INSERT INTO "groupPermission" ("group_id", "permission") VALUES
('editors', 'custom-widget-manage'),
('editors', 'custom-widget-secret-write'),
('editors', 'board-create');
`);
await pool.query(`INSERT INTO "item" ("id", "kind", "options") VALUES ('weather-item', 'customApi', $1)`, [
legacyPlacementOptions,
]);
await applyMigration(pool, "0010_custom_widget_v2_tables.sql");
await pool.query(`
INSERT INTO "custom_widget_v2_definition" (
"id", "name", "sources", "requests", "options", "template", "enabled",
"created_at", "updated_at", "creator_id"
) VALUES (
'legacy-weather', 'Weather v2', '[]', '[]', '[]', '<Text>Weather</Text>', true,
'2023-11-14 00:00:02', '2023-11-14 00:00:03', 'owner'
);
INSERT INTO "custom_widget_v2_secret" (
"source_id", "kind", "encrypted_value", "updated_at", "definition_id"
) VALUES ('weather', 'apiKey', 'encrypted.v2-value', '2023-11-14 00:00:03', 'legacy-weather');
`);
expect(
(await pool.query("SELECT id, name, url, enabled, creator_id FROM custom_widget_definition")).rows,
).toEqual([
expect.objectContaining({
id: "legacy-weather",
name: "Weather",
url: "https://example.test/weather",
enabled: true,
creator_id: "owner",
}),
]);
expect((await pool.query("SELECT definition_id, kind, value FROM custom_widget_secret")).rows).toEqual([
expect.objectContaining({ definition_id: "legacy-weather", kind: "apiKey", value: "encrypted.value" }),
]);
expect((await pool.query("SELECT id, name, template FROM custom_widget_v2_definition")).rows).toEqual([
expect.objectContaining({ id: "legacy-weather", name: "Weather v2", template: "<Text>Weather</Text>" }),
]);
expect(
(await pool.query("SELECT definition_id, source_id, kind, encrypted_value FROM custom_widget_v2_secret")).rows,
).toEqual([
expect.objectContaining({
definition_id: "legacy-weather",
source_id: "weather",
kind: "apiKey",
encrypted_value: "encrypted.v2-value",
}),
]);
expect((await pool.query("SELECT options FROM item WHERE id = 'weather-item'")).rows).toEqual([
{ options: legacyPlacementOptions },
]);
expect((await pool.query('SELECT "permission" FROM "groupPermission" ORDER BY "permission"')).rows).toEqual([
{ permission: "board-create" },
]);
const v1Columns = (
await pool.query(
"SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'custom_widget_definition'",
)
).rows.map((column: { column_name: string }) => column.column_name);
expect(v1Columns).toEqual(expect.arrayContaining(["id", "url", "auth_type", "display_type", "display_config"]));
expect(v1Columns).not.toContain("sources");
expect(
(
await pool.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE 'legacy_custom_widget%'",
)
).rows,
).toEqual([]);
} finally {
await pool.end();
await postgreSqlContainer.stop();
}
}, 40_000);
});
+4 -2
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable import/namespace -- The test compares matching schema tables by their shared keys. */
/* eslint-disable import/namespace -- This test compares equivalent schema exports by shared dynamic keys. */
import type { Column, InferSelectModel } from "drizzle-orm";
import type { ForeignKey as MysqlForeignKey, MySqlTableWithColumns } from "drizzle-orm/mysql-core";
import type { PgTableWithColumns, ForeignKey as PostgresqlForeignKey } from "drizzle-orm/pg-core";
@@ -24,7 +24,6 @@ type FixedMysqlConfig = {
};
};
};
type FixedPostgresqlConfig = {
[key in keyof PostgreisqlConfig]: {
[column in keyof PostgreisqlConfig[key]]: {
@@ -64,6 +63,7 @@ test("schemas should match", () => {
if (!("uniqueName" in sqliteColumn)) return;
if (!("primary" in sqliteColumn)) return;
// oxlint-disable-next-line import/namespace -- this test intentionally compares matching dynamic schema keys.
const mysqlTable = mysqlSchema[tableName];
const mysqlColumn = mysqlTable[columnName as keyof typeof mysqlTable] as object;
@@ -85,6 +85,7 @@ test("schemas should match", () => {
).toEqual(mysqlColumn.primary);
});
// oxlint-disable-next-line import/namespace -- this test intentionally compares matching dynamic schema keys.
const mysqlTable = mysqlSchema[tableName];
const sqliteForeignKeys = sqliteTable[Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable] as
| SqliteForeignKey[]
@@ -145,6 +146,7 @@ test("schemas should match for postgresql", () => {
objectEntries(sqliteSchema).forEach(([tableName, sqliteTable]) => {
// keys of sqliteSchema and postgresqlSchema are the same, so we can safely use tableName as key
// skipcq: JS-E1007
// oxlint-disable-next-line import/namespace -- this test intentionally compares matching dynamic schema keys.
const postgresqlTable = postgresqlSchema[tableName];
Object.entries(sqliteTable).forEach(([columnName, sqliteColumn]: [string, object]) => {
if (!("isUnique" in sqliteColumn)) return;
+164
View File
@@ -0,0 +1,164 @@
import path from "path";
import { readFileSync } from "node:fs";
import BetterSqlite3 from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import SuperJSON from "superjson";
import { expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import type { Database } from "..";
import { seedDataAsync } from "../migrations/seed";
import * as sqliteSchema from "../schema/sqlite";
import { expectBundledCustomWidgetsSeeded } from "./custom-widget-seed-assertions";
const applyMigration = (connection: BetterSqlite3.Database, fileName: string) => {
const migration = readFileSync(path.join(__dirname, "..", "migrations", "sqlite", fileName), "utf8");
for (const statement of migration
.split("--> statement-breakpoint")
.map((value) => value.trim())
.filter(Boolean)) {
connection.exec(statement);
}
};
test("SQLite migrations seed the five disabled bundled custom widgets", async () => {
const connection = new BetterSqlite3(":memory:");
const database = drizzle(connection, { schema: sqliteSchema, casing: DB_CASING });
migrate(database, { migrationsFolder: path.join(__dirname, "..", "migrations", "sqlite") });
await seedDataAsync(database as unknown as Database);
await expectBundledCustomWidgetsSeeded(database as unknown as Database);
expect(
connection
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'custom_widget%' ORDER BY name")
.all(),
).toEqual([
{ name: "custom_widget_definition" },
{ name: "custom_widget_secret" },
{ name: "custom_widget_v2_definition" },
{ name: "custom_widget_v2_secret" },
]);
connection.close();
});
test("Custom Widget v2 migration preserves v1 data and board references", () => {
const connection = new BetterSqlite3(":memory:");
const legacyPlacementOptions = SuperJSON.stringify({ definitionId: "legacy-weather", refreshInterval: 30 });
connection.exec(`
CREATE TABLE user (id text PRIMARY KEY NOT NULL);
CREATE TABLE groupPermission (
group_id text NOT NULL,
permission text NOT NULL
);
CREATE TABLE item (
id text PRIMARY KEY NOT NULL,
kind text NOT NULL,
options text NOT NULL
);
CREATE TABLE custom_widget_definition (
id text PRIMARY KEY NOT NULL,
name text NOT NULL,
description text,
icon_url text,
url text NOT NULL,
auth_type text DEFAULT 'none' NOT NULL,
header_name text,
method text DEFAULT 'GET' NOT NULL,
request_body text,
display_type text DEFAULT 'singleValue' NOT NULL,
display_config text DEFAULT '{}' NOT NULL,
enabled integer DEFAULT true NOT NULL,
created_at integer NOT NULL,
updated_at integer NOT NULL,
creator_id text REFERENCES user(id) ON DELETE SET NULL
);
CREATE TABLE custom_widget_secret (
kind text NOT NULL,
value text NOT NULL,
updated_at integer NOT NULL,
definition_id text NOT NULL REFERENCES custom_widget_definition(id) ON DELETE CASCADE,
PRIMARY KEY(definition_id, kind)
);
INSERT INTO user (id) VALUES ('owner');
INSERT INTO custom_widget_definition (
id, name, url, auth_type, method, display_type, display_config, enabled, created_at, updated_at, creator_id
) VALUES ('legacy-weather', 'Weather', 'https://example.test/weather', 'bearer', 'GET', 'singleValue',
'{"type":"singleValue","jsonPath":"$.temperature"}', true, 1700000000, 1700000001, 'owner');
INSERT INTO custom_widget_secret (kind, value, updated_at, definition_id)
VALUES ('apiKey', 'encrypted.value', 1700000001, 'legacy-weather');
INSERT INTO groupPermission (group_id, permission) VALUES
('editors', 'custom-widget-manage'),
('editors', 'custom-widget-secret-write'),
('editors', 'board-create');
`);
connection
.prepare("INSERT INTO item (id, kind, options) VALUES ('weather-item', 'customApi', ?)")
.run(legacyPlacementOptions);
applyMigration(connection, "0042_custom_widget_v2_tables.sql");
connection.exec(`
INSERT INTO custom_widget_v2_definition (
id, name, sources, requests, options, template, enabled, created_at, updated_at, creator_id
) VALUES (
'legacy-weather', 'Weather v2', '[]', '[]', '[]', '<Text>Weather</Text>', true,
1700000002, 1700000003, 'owner'
);
INSERT INTO custom_widget_v2_secret (source_id, kind, encrypted_value, updated_at, definition_id)
VALUES ('weather', 'apiKey', 'encrypted.v2-value', 1700000003, 'legacy-weather');
`);
expect(connection.prepare("SELECT * FROM custom_widget_definition").get()).toMatchObject({
id: "legacy-weather",
name: "Weather",
url: "https://example.test/weather",
enabled: 1,
creator_id: "owner",
});
expect(connection.prepare("SELECT * FROM custom_widget_secret").get()).toMatchObject({
definition_id: "legacy-weather",
kind: "apiKey",
value: "encrypted.value",
});
expect(connection.prepare("SELECT * FROM custom_widget_v2_definition").get()).toMatchObject({
id: "legacy-weather",
name: "Weather v2",
template: "<Text>Weather</Text>",
});
expect(connection.prepare("SELECT * FROM custom_widget_v2_secret").get()).toMatchObject({
definition_id: "legacy-weather",
source_id: "weather",
encrypted_value: "encrypted.v2-value",
});
expect(connection.prepare("SELECT options FROM item WHERE id = 'weather-item'").get()).toEqual({
options: legacyPlacementOptions,
});
expect(connection.prepare("SELECT permission FROM groupPermission ORDER BY permission").all()).toEqual([
{ permission: "board-create" },
]);
expect(
connection
.prepare(
`SELECT id, name, url, auth_type, header_name, method, request_body, display_type, display_config,
enabled, created_at, updated_at, creator_id
FROM custom_widget_definition`,
)
.get(),
).toMatchObject({
id: "legacy-weather",
auth_type: "bearer",
display_type: "singleValue",
});
expect(
connection
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'legacy_custom_widget%' ORDER BY name",
)
.all(),
).toEqual([]);
connection.close();
});
+1 -1
View File
@@ -4,5 +4,5 @@
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
"exclude": ["node_modules", "migrations/*/migrate.cjs"]
}