mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: rebuild workspaces as the reference plugin
This commit is contained in:
1 parent
d774df8b41
commit
f217d8db89
79 files changed
+32995
-2184
No files matched your search
@@ -2839,36 +2839,6 @@ const migrateSchema = () => {
|
||||
}
|
||||
// --- fleets end ---
|
||||
|
||||
// --- workspaces begin ---
|
||||
try {
|
||||
sqlite.prepare("SELECT id FROM user_workspaces LIMIT 1").get();
|
||||
} catch {
|
||||
try {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_workspaces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT,
|
||||
icon TEXT,
|
||||
kind TEXT NOT NULL DEFAULT 'manual',
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
payload TEXT NOT NULL DEFAULT '{}',
|
||||
sync_id TEXT UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TEXT
|
||||
);
|
||||
`);
|
||||
} catch (createError) {
|
||||
databaseLogger.warn("Failed to create user_workspaces table", {
|
||||
operation: "schema_migration",
|
||||
error: createError,
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- workspaces end ---
|
||||
|
||||
// --- sync begin ---
|
||||
// Stable per-row identity used to match rows across two independently-
|
||||
// seeded databases (the embedded desktop backend and a connected remote
|
||||
|
||||
@@ -152,11 +152,6 @@ export const PERFORMANCE_INDEXES: PerformanceIndex[] = [
|
||||
table: "user_open_tabs",
|
||||
columns: "user_id",
|
||||
},
|
||||
{
|
||||
name: "idx_user_workspaces_user_id",
|
||||
table: "user_workspaces",
|
||||
columns: "user_id",
|
||||
},
|
||||
{
|
||||
name: "idx_homepage_items_user_id",
|
||||
table: "homepage_items",
|
||||
|
||||
@@ -1780,36 +1780,7 @@ export const fleetInventory = mysqlTable(
|
||||
);
|
||||
// --- fleets end ---
|
||||
|
||||
// --- workspaces begin ---
|
||||
export const userWorkspaces = mysqlTable(
|
||||
"user_workspaces",
|
||||
{
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
// "manual" | "last_session" - exactly one last_session row per user.
|
||||
kind: text("kind").notNull().default("manual"),
|
||||
isDefault: boolean("is_default")
|
||||
.notNull()
|
||||
.default(false),
|
||||
// JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes
|
||||
payload: text("payload").notNull().default("{}"),
|
||||
syncId: varchar("sync_id", { length: 255 }).unique(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updatedAt: varchar("updated_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
},
|
||||
(table) => [index("idx_user_workspaces_user_id").on(table.userId)],
|
||||
);
|
||||
// --- workspaces end ---
|
||||
|
||||
|
||||
// --- sync begin ---
|
||||
// Records a delete for a synced entity type so the other side of a sync
|
||||
|
||||
@@ -1781,36 +1781,7 @@ export const fleetInventory = pgTable(
|
||||
);
|
||||
// --- fleets end ---
|
||||
|
||||
// --- workspaces begin ---
|
||||
export const userWorkspaces = pgTable(
|
||||
"user_workspaces",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
// "manual" | "last_session" - exactly one last_session row per user.
|
||||
kind: text("kind").notNull().default("manual"),
|
||||
isDefault: boolean("is_default")
|
||||
.notNull()
|
||||
.default(false),
|
||||
// JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes
|
||||
payload: text("payload").notNull().default("{}"),
|
||||
syncId: varchar("sync_id", { length: 255 }).unique(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: varchar("updated_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
},
|
||||
(table) => [index("idx_user_workspaces_user_id").on(table.userId)],
|
||||
);
|
||||
// --- workspaces end ---
|
||||
|
||||
|
||||
// --- sync begin ---
|
||||
// Records a delete for a synced entity type so the other side of a sync
|
||||
|
||||
@@ -1779,36 +1779,7 @@ export const fleetInventory = sqliteTable(
|
||||
);
|
||||
// --- fleets end ---
|
||||
|
||||
// --- workspaces begin ---
|
||||
export const userWorkspaces = sqliteTable(
|
||||
"user_workspaces",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
color: text("color"),
|
||||
icon: text("icon"),
|
||||
// "manual" | "last_session" - exactly one last_session row per user.
|
||||
kind: text("kind").notNull().default("manual"),
|
||||
isDefault: integer("is_default", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
// JSON-encoded WorkspacePayload: tabs, splitMode, paneTabIds, rowSizes, rowColSizes
|
||||
payload: text("payload").notNull().default("{}"),
|
||||
syncId: text("sync_id").unique(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
lastUsedAt: text("last_used_at"),
|
||||
},
|
||||
(table) => [index("idx_user_workspaces_user_id").on(table.userId)],
|
||||
);
|
||||
// --- workspaces end ---
|
||||
|
||||
|
||||
// --- sync begin ---
|
||||
// Records a delete for a synced entity type so the other side of a sync
|
||||
|
||||
@@ -69,7 +69,6 @@ import { UserPreferenceRepository } from "./user-preference-repository.js";
|
||||
import { UserRepository } from "./user-repository.js";
|
||||
import { VaultProfileRepository } from "./vault-profile-repository.js";
|
||||
import { VaultTokenRepository } from "./vault-token-repository.js";
|
||||
import { WorkspaceRepository } from "./workspace-repository.js";
|
||||
|
||||
/**
|
||||
* The context every repository runs against.
|
||||
@@ -631,13 +630,6 @@ export function createCurrentVaultTokenRepository(): VaultTokenRepository {
|
||||
);
|
||||
}
|
||||
|
||||
export function createCurrentWorkspaceRepository(): WorkspaceRepository {
|
||||
return new WorkspaceRepository(
|
||||
createCurrentRepositoryContext(),
|
||||
createCurrentRepositoryWriteHook("workspace_repository_write"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the settings cache. Must run during startup on engines without a
|
||||
* synchronous read, before anything calls getCurrentSettingValue.
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import { and, eq, ne } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
import { userWorkspaces } from "../db/schema.js";
|
||||
import type { DatabaseContext } from "./database-context.js";
|
||||
import { rowsAffected } from "./mutation-result.js";
|
||||
import { insertReturning, updateReturning } from "./returning.js";
|
||||
|
||||
export type WorkspaceRecord = typeof userWorkspaces.$inferSelect;
|
||||
|
||||
export interface WorkspaceCreateInput {
|
||||
name: string;
|
||||
color?: string | null;
|
||||
icon?: string | null;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceUpdateInput {
|
||||
name?: string;
|
||||
color?: string | null;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
export class WorkspaceRepository {
|
||||
constructor(
|
||||
private readonly context: DatabaseContext,
|
||||
private readonly onWrite?: () => void | Promise<void>,
|
||||
) {}
|
||||
|
||||
async listByUser(userId: string): Promise<WorkspaceRecord[]> {
|
||||
return this.context.drizzle
|
||||
.select()
|
||||
.from(userWorkspaces)
|
||||
.where(eq(userWorkspaces.userId, userId));
|
||||
}
|
||||
|
||||
async findById(userId: string, id: number): Promise<WorkspaceRecord | null> {
|
||||
const rows = await this.context.drizzle
|
||||
.select()
|
||||
.from(userWorkspaces)
|
||||
.where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async findLastSession(userId: string): Promise<WorkspaceRecord | null> {
|
||||
const rows = await this.context.drizzle
|
||||
.select()
|
||||
.from(userWorkspaces)
|
||||
.where(
|
||||
and(
|
||||
eq(userWorkspaces.userId, userId),
|
||||
eq(userWorkspaces.kind, "last_session"),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async findDefault(userId: string): Promise<WorkspaceRecord | null> {
|
||||
const rows = await this.context.drizzle
|
||||
.select()
|
||||
.from(userWorkspaces)
|
||||
.where(
|
||||
and(
|
||||
eq(userWorkspaces.userId, userId),
|
||||
eq(userWorkspaces.isDefault, true),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async upsertLastSession(
|
||||
userId: string,
|
||||
payload: string,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord> {
|
||||
const existing = await this.findLastSession(userId);
|
||||
|
||||
if (!existing) {
|
||||
const [created] = await insertReturning(this.context, userWorkspaces, {
|
||||
userId,
|
||||
name: "Last Session",
|
||||
color: null,
|
||||
icon: null,
|
||||
kind: "last_session",
|
||||
isDefault: false,
|
||||
payload,
|
||||
syncId: randomUUID(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await this.afterWrite();
|
||||
return created;
|
||||
}
|
||||
|
||||
const [updated] = await updateReturning(
|
||||
this.context,
|
||||
userWorkspaces,
|
||||
{ payload, updatedAt: now },
|
||||
and(
|
||||
eq(userWorkspaces.id, existing.id),
|
||||
eq(userWorkspaces.userId, userId),
|
||||
),
|
||||
);
|
||||
await this.afterWrite();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
input: WorkspaceCreateInput,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord> {
|
||||
const [created] = await insertReturning(this.context, userWorkspaces, {
|
||||
userId,
|
||||
name: input.name,
|
||||
color: input.color ?? null,
|
||||
icon: input.icon ?? null,
|
||||
kind: "manual",
|
||||
isDefault: false,
|
||||
payload: input.payload,
|
||||
syncId: randomUUID(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await this.afterWrite();
|
||||
return created;
|
||||
}
|
||||
|
||||
async update(
|
||||
userId: string,
|
||||
id: number,
|
||||
input: WorkspaceUpdateInput,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord | null> {
|
||||
const existing = await this.findById(userId, id);
|
||||
if (!existing || existing.kind !== "manual") return null;
|
||||
|
||||
const [updated] = await updateReturning(
|
||||
this.context,
|
||||
userWorkspaces,
|
||||
{
|
||||
name: input.name ?? existing.name,
|
||||
color: input.color === undefined ? existing.color : input.color,
|
||||
icon: input.icon === undefined ? existing.icon : input.icon,
|
||||
updatedAt: now,
|
||||
},
|
||||
and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)),
|
||||
);
|
||||
await this.afterWrite();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async updateContent(
|
||||
userId: string,
|
||||
id: number,
|
||||
payload: string,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord | null> {
|
||||
const existing = await this.findById(userId, id);
|
||||
if (!existing || existing.kind !== "manual") return null;
|
||||
|
||||
const [updated] = await updateReturning(
|
||||
this.context,
|
||||
userWorkspaces,
|
||||
{ payload, updatedAt: now },
|
||||
and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)),
|
||||
);
|
||||
await this.afterWrite();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async setDefault(
|
||||
userId: string,
|
||||
id: number,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord | null> {
|
||||
const existing = await this.findById(userId, id);
|
||||
if (!existing || existing.kind !== "manual") return null;
|
||||
|
||||
await this.context.drizzle
|
||||
.update(userWorkspaces)
|
||||
.set({ isDefault: false, updatedAt: now })
|
||||
.where(
|
||||
and(
|
||||
eq(userWorkspaces.userId, userId),
|
||||
eq(userWorkspaces.isDefault, true),
|
||||
ne(userWorkspaces.id, id),
|
||||
),
|
||||
);
|
||||
|
||||
const [updated] = await updateReturning(
|
||||
this.context,
|
||||
userWorkspaces,
|
||||
{ isDefault: true, updatedAt: now },
|
||||
and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)),
|
||||
);
|
||||
await this.afterWrite();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async unsetDefault(
|
||||
userId: string,
|
||||
id: number,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<WorkspaceRecord | null> {
|
||||
const existing = await this.findById(userId, id);
|
||||
if (!existing || existing.kind !== "manual") return null;
|
||||
|
||||
const [updated] = await updateReturning(
|
||||
this.context,
|
||||
userWorkspaces,
|
||||
{ isDefault: false, updatedAt: now },
|
||||
and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)),
|
||||
);
|
||||
await this.afterWrite();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async touchLastUsed(
|
||||
userId: string,
|
||||
id: number,
|
||||
now = new Date().toISOString(),
|
||||
): Promise<void> {
|
||||
const result = await this.context.drizzle
|
||||
.update(userWorkspaces)
|
||||
.set({ lastUsedAt: now })
|
||||
.where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)));
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
}
|
||||
|
||||
async delete(userId: string, id: number): Promise<boolean> {
|
||||
const existing = await this.findById(userId, id);
|
||||
if (!existing || existing.kind !== "manual") return false;
|
||||
|
||||
const result = await this.context.drizzle
|
||||
.delete(userWorkspaces)
|
||||
.where(and(eq(userWorkspaces.id, id), eq(userWorkspaces.userId, userId)));
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<number> {
|
||||
const owned = await this.listByUser(userId);
|
||||
const result = await this.context.drizzle
|
||||
.delete(userWorkspaces)
|
||||
.where(eq(userWorkspaces.userId, userId));
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
return owned.length;
|
||||
}
|
||||
|
||||
private async afterWrite(): Promise<void> {
|
||||
await this.onWrite?.();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
type SyncReferenceEntity,
|
||||
} from "./sync-references.js";
|
||||
import { timestampAtOrAfter } from "../sync-timestamp.js";
|
||||
import {
|
||||
insertReturningWhere,
|
||||
updateReturning,
|
||||
} from "../repositories/returning.js";
|
||||
import { validateParentHostId } from "./host-parent-validation.js";
|
||||
|
||||
// Primed at import so every consumer of this module - the routes below, the
|
||||
@@ -58,9 +62,24 @@ function entityConfig(entityType: SyncEntityType) {
|
||||
table: entity.table as SyncTable,
|
||||
readOnlyFields: entity.readOnlyFields,
|
||||
singleton: entity.singleton === true,
|
||||
shouldSync: (row: Record<string, unknown>) => isSyncedRow(entityType, row),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a row takes part in sync. An entity can leave some of its rows out
|
||||
* (a per-install "last session"): they are not pulled, and a push, update or
|
||||
* delete aimed at one is refused.
|
||||
*/
|
||||
export function isSyncedRow(
|
||||
entityType: string,
|
||||
row: Record<string, unknown>,
|
||||
): boolean {
|
||||
const entity = getEntity(entityType);
|
||||
if (!entity?.shouldSync) return true;
|
||||
return entity.shouldSync(row) !== false;
|
||||
}
|
||||
|
||||
type RepositoryContext = ReturnType<typeof createCurrentRepositoryContext>;
|
||||
|
||||
export function isValidEntityType(value: unknown): value is SyncEntityType {
|
||||
@@ -259,7 +278,7 @@ router.get(
|
||||
: null;
|
||||
|
||||
try {
|
||||
const { table, singleton } = entityConfig(entityType);
|
||||
const { table, singleton, shouldSync } = entityConfig(entityType);
|
||||
const context = createCurrentRepositoryContext();
|
||||
const conditions = [eq(table.userId, userId)];
|
||||
if (since && "updatedAt" in table) {
|
||||
@@ -273,8 +292,10 @@ router.get(
|
||||
.from(table as typeof hosts)
|
||||
.where(and(...conditions));
|
||||
|
||||
const synced = (rows as Record<string, unknown>[]).filter(shouldSync);
|
||||
|
||||
const decrypted = await Promise.all(
|
||||
rows.map(async (row) => {
|
||||
synced.map(async (row) => {
|
||||
const result = await serializeSyncReferences(
|
||||
entityType,
|
||||
decryptIfNeeded(entityType, row as Record<string, unknown>, userId),
|
||||
@@ -331,12 +352,20 @@ router.post(
|
||||
}
|
||||
|
||||
try {
|
||||
const { table } = entityConfig(entityType);
|
||||
const { table, shouldSync } = entityConfig(entityType);
|
||||
const context = createCurrentRepositoryContext();
|
||||
const locateRow = locateSyncRow(entityType, userId, syncId);
|
||||
|
||||
await context.drizzle
|
||||
.delete(table as typeof hosts)
|
||||
.where(locateSyncRow(entityType, userId, syncId));
|
||||
const [target] = (await context.drizzle
|
||||
.select()
|
||||
.from(table as typeof hosts)
|
||||
.where(locateRow)
|
||||
.limit(1)) as Record<string, unknown>[];
|
||||
if (target && !shouldSync(target)) {
|
||||
return res.status(400).json({ error: "This row is not synced" });
|
||||
}
|
||||
|
||||
await context.drizzle.delete(table as typeof hosts).where(locateRow);
|
||||
|
||||
await createCurrentSyncTombstoneRepository().record(
|
||||
userId,
|
||||
@@ -397,10 +426,13 @@ router.post(
|
||||
try {
|
||||
// singleton is still needed below: those tables have no sync_id column
|
||||
// for the insert to populate.
|
||||
const { table, singleton } = entityConfig(entityType);
|
||||
const { table, singleton, shouldSync } = entityConfig(entityType);
|
||||
const context = createCurrentRepositoryContext();
|
||||
|
||||
const locateRow = locateSyncRow(entityType, userId, syncId);
|
||||
if (!shouldSync(payload)) {
|
||||
return res.status(400).json({ error: "This row is not synced" });
|
||||
}
|
||||
|
||||
const existingRows = await context.drizzle
|
||||
.select()
|
||||
@@ -408,6 +440,9 @@ router.post(
|
||||
.where(locateRow)
|
||||
.limit(1);
|
||||
const existing = existingRows[0] as Record<string, unknown> | undefined;
|
||||
if (existing && !shouldSync(existing)) {
|
||||
return res.status(400).json({ error: "This row is not synced" });
|
||||
}
|
||||
|
||||
const resolvedPayload = await deserializeSyncReferences(
|
||||
entityType,
|
||||
@@ -433,27 +468,30 @@ router.post(
|
||||
userId,
|
||||
);
|
||||
|
||||
// Through the returning helpers, because MySQL has no RETURNING and a
|
||||
// bare .returning() throws there.
|
||||
let resultRow: Record<string, unknown>;
|
||||
if (existing) {
|
||||
const updatedRows = await context.drizzle
|
||||
.update(table as typeof hosts)
|
||||
.set(encryptedPayload)
|
||||
.where(locateRow)
|
||||
.returning();
|
||||
const updatedRows = await updateReturning(
|
||||
context,
|
||||
table as typeof hosts,
|
||||
encryptedPayload,
|
||||
locateRow,
|
||||
);
|
||||
resultRow = updatedRows[0] as Record<string, unknown>;
|
||||
} else {
|
||||
const insertedRows = await context.drizzle
|
||||
.insert(table as typeof hosts)
|
||||
.values(
|
||||
(singleton
|
||||
? { ...encryptedPayload, userId }
|
||||
: {
|
||||
...encryptedPayload,
|
||||
userId,
|
||||
syncId,
|
||||
}) as typeof hosts.$inferInsert,
|
||||
)
|
||||
.returning();
|
||||
const insertedRows = await insertReturningWhere(
|
||||
context,
|
||||
table as typeof hosts,
|
||||
(singleton
|
||||
? { ...encryptedPayload, userId }
|
||||
: {
|
||||
...encryptedPayload,
|
||||
userId,
|
||||
syncId,
|
||||
}) as typeof hosts.$inferInsert,
|
||||
locateRow,
|
||||
);
|
||||
resultRow = insertedRows[0] as Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user