feat(storage): complete phase 3 branding foundation

This commit is contained in:
poyrazavsever
2026-07-16 17:05:59 +03:00
parent b155132acf
commit 5d863280bf
31 changed files with 5302 additions and 57 deletions
@@ -0,0 +1,68 @@
CREATE TABLE `files` (
`id` text PRIMARY KEY NOT NULL,
`owner_user_id` text NOT NULL,
`uploaded_by_user_id` text NOT NULL,
`auth_user_id` text,
`project_id` text,
`kind` text NOT NULL,
`visibility` text DEFAULT 'private' NOT NULL,
`storage_path` text NOT NULL,
`original_name` text NOT NULL,
`mime_type` text NOT NULL,
`byte_size` integer NOT NULL,
`sha256` text NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
FOREIGN KEY (`owner_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE restrict,
FOREIGN KEY (`uploaded_by_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE restrict,
FOREIGN KEY (`auth_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE restrict,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE restrict,
CONSTRAINT "files_kind_check" CHECK("files"."kind" in ('avatar', 'branding_logo', 'branding_icon', 'project_asset')),
CONSTRAINT "files_visibility_check" CHECK("files"."visibility" in ('private', 'portal', 'public_branding')),
CONSTRAINT "files_byte_size_check" CHECK("files"."byte_size" > 0),
CONSTRAINT "files_sha256_check" CHECK(length("files"."sha256") = 64),
CONSTRAINT "files_storage_path_check" CHECK("files"."storage_path" not like '/%' and instr("files"."storage_path", '..') = 0),
CONSTRAINT "files_resource_check" CHECK((
("files"."kind" = 'avatar' and "files"."auth_user_id" is not null and "files"."project_id" is null and "files"."visibility" = 'private')
or ("files"."kind" in ('branding_logo', 'branding_icon') and "files"."auth_user_id" is null and "files"."project_id" is null and "files"."visibility" = 'public_branding')
or ("files"."kind" = 'project_asset' and "files"."auth_user_id" is null and "files"."project_id" is not null and "files"."visibility" in ('private', 'portal'))
))
);
--> statement-breakpoint
CREATE UNIQUE INDEX `files_storage_path_unique` ON `files` (`storage_path`);--> statement-breakpoint
CREATE INDEX `files_owner_kind_idx` ON `files` (`owner_user_id`,`kind`);--> statement-breakpoint
CREATE INDEX `files_auth_user_id_idx` ON `files` (`auth_user_id`);--> statement-breakpoint
CREATE INDEX `files_project_id_idx` ON `files` (`project_id`);--> statement-breakpoint
CREATE INDEX `files_sha256_idx` ON `files` (`sha256`);--> statement-breakpoint
CREATE TABLE `instance_branding` (
`id` text PRIMARY KEY DEFAULT 'default' NOT NULL,
`owner_user_id` text NOT NULL,
`application_name` text DEFAULT 'Neta' NOT NULL,
`short_name` text DEFAULT 'Neta' NOT NULL,
`primary_color` text DEFAULT '#C81E1E' NOT NULL,
`accent_color` text DEFAULT '#E6EDF5' NOT NULL,
`light_logo_file_id` text,
`dark_logo_file_id` text,
`icon_file_id` text,
`default_color_mode` text DEFAULT 'system' NOT NULL,
`radius_scale` text DEFAULT 'default' NOT NULL,
`organization_name` text,
`support_email` text,
`portal_welcome_text` text,
`portal_footer_text` text,
`updated_by_user_id` text NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
FOREIGN KEY (`owner_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE restrict,
FOREIGN KEY (`light_logo_file_id`) REFERENCES `files`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`dark_logo_file_id`) REFERENCES `files`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`icon_file_id`) REFERENCES `files`(`id`) ON UPDATE no action ON DELETE set null,
FOREIGN KEY (`updated_by_user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE restrict,
CONSTRAINT "instance_branding_id_check" CHECK("instance_branding"."id" = 'default'),
CONSTRAINT "instance_branding_primary_color_check" CHECK("instance_branding"."primary_color" glob '#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]'),
CONSTRAINT "instance_branding_accent_color_check" CHECK("instance_branding"."accent_color" glob '#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]'),
CONSTRAINT "instance_branding_color_mode_check" CHECK("instance_branding"."default_color_mode" in ('light', 'dark', 'system')),
CONSTRAINT "instance_branding_radius_scale_check" CHECK("instance_branding"."radius_scale" in ('compact', 'default', 'soft'))
);
--> statement-breakpoint
CREATE UNIQUE INDEX `instance_branding_owner_unique` ON `instance_branding` (`owner_user_id`);
File diff suppressed because it is too large Load Diff
+7
View File
@@ -29,6 +29,13 @@
"when": 1784208712933,
"tag": "0003_chief_excalibur",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1784210311370,
"tag": "0004_fancy_baron_zemo",
"breakpoints": true
}
]
}
+1
View File
@@ -1,3 +1,4 @@
export * from "./auth";
export * from "./domain";
export * from "./runtime";
export * from "./storage";
+104
View File
@@ -0,0 +1,104 @@
import { sql } from "drizzle-orm";
import { check, index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
import type {
BrandingColorMode,
BrandingRadiusScale,
FileKind,
FileVisibility,
} from "../../domain/types";
import { user } from "./auth";
import { projects } from "./domain";
const nowMs = sql`(cast(unixepoch('subsecond') * 1000 as integer))`;
export const files = sqliteTable(
"files",
{
id: text("id").primaryKey(),
ownerUserId: text("owner_user_id")
.notNull()
.references(() => user.id, { onDelete: "restrict" }),
uploadedByUserId: text("uploaded_by_user_id")
.notNull()
.references(() => user.id, { onDelete: "restrict" }),
authUserId: text("auth_user_id").references(() => user.id, { onDelete: "restrict" }),
projectId: text("project_id").references(() => projects.id, { onDelete: "restrict" }),
kind: text("kind").$type<FileKind>().notNull(),
visibility: text("visibility").$type<FileVisibility>().default("private").notNull(),
storagePath: text("storage_path").notNull(),
originalName: text("original_name").notNull(),
mimeType: text("mime_type").notNull(),
byteSize: integer("byte_size").notNull(),
sha256: text("sha256").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(nowMs)
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
uniqueIndex("files_storage_path_unique").on(table.storagePath),
index("files_owner_kind_idx").on(table.ownerUserId, table.kind),
index("files_auth_user_id_idx").on(table.authUserId),
index("files_project_id_idx").on(table.projectId),
index("files_sha256_idx").on(table.sha256),
check(
"files_kind_check",
sql`${table.kind} in ('avatar', 'branding_logo', 'branding_icon', 'project_asset')`,
),
check(
"files_visibility_check",
sql`${table.visibility} in ('private', 'portal', 'public_branding')`,
),
check("files_byte_size_check", sql`${table.byteSize} > 0`),
check("files_sha256_check", sql`length(${table.sha256}) = 64`),
check("files_storage_path_check", sql`${table.storagePath} not like '/%' and instr(${table.storagePath}, '..') = 0`),
check(
"files_resource_check",
sql`(
(${table.kind} = 'avatar' and ${table.authUserId} is not null and ${table.projectId} is null and ${table.visibility} = 'private')
or (${table.kind} in ('branding_logo', 'branding_icon') and ${table.authUserId} is null and ${table.projectId} is null and ${table.visibility} = 'public_branding')
or (${table.kind} = 'project_asset' and ${table.authUserId} is null and ${table.projectId} is not null and ${table.visibility} in ('private', 'portal'))
)`,
),
],
);
export const instanceBranding = sqliteTable(
"instance_branding",
{
id: text("id").primaryKey().default("default"),
ownerUserId: text("owner_user_id")
.notNull()
.references(() => user.id, { onDelete: "restrict" }),
applicationName: text("application_name").default("Neta").notNull(),
shortName: text("short_name").default("Neta").notNull(),
primaryColor: text("primary_color").default("#C81E1E").notNull(),
accentColor: text("accent_color").default("#E6EDF5").notNull(),
lightLogoFileId: text("light_logo_file_id").references(() => files.id, { onDelete: "set null" }),
darkLogoFileId: text("dark_logo_file_id").references(() => files.id, { onDelete: "set null" }),
iconFileId: text("icon_file_id").references(() => files.id, { onDelete: "set null" }),
defaultColorMode: text("default_color_mode").$type<BrandingColorMode>().default("system").notNull(),
radiusScale: text("radius_scale").$type<BrandingRadiusScale>().default("default").notNull(),
organizationName: text("organization_name"),
supportEmail: text("support_email"),
portalWelcomeText: text("portal_welcome_text"),
portalFooterText: text("portal_footer_text"),
updatedByUserId: text("updated_by_user_id")
.notNull()
.references(() => user.id, { onDelete: "restrict" }),
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(nowMs).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(nowMs)
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
uniqueIndex("instance_branding_owner_unique").on(table.ownerUserId),
check("instance_branding_id_check", sql`${table.id} = 'default'`),
check("instance_branding_primary_color_check", sql`${table.primaryColor} glob '#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]'`),
check("instance_branding_accent_color_check", sql`${table.accentColor} glob '#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]'`),
check("instance_branding_color_mode_check", sql`${table.defaultColorMode} in ('light', 'dark', 'system')`),
check("instance_branding_radius_scale_check", sql`${table.radiusScale} in ('compact', 'default', 'soft')`),
],
);