SPB Git
24.0 KB · 237 lines typescript
1import { and, eq, inArray, isNull, or, sql as dsql } from "drizzle-orm";2import { db, schema } from "@/db";3import { HttpError } from "@/lib/auth/guard";4import { createPage, softDeletePage } from "@/lib/pages/service";5import { enqueue, QUEUES } from "@/lib/jobs";6import type { Database, Property, PropertyType, PropertyConfig, View, ViewType, ViewConfig, RowOut, SelectOption, RelationRef } from "./types";7import { PROPERTY_TYPES, COMPUTED_TYPES, VIEW_TYPES } from "./types";8import { hydrate, loadProps, loadRaw, queryRows } from "./query";9import { displayValue, ensureOption, newId, numberOf, titleProp, isEmpty } from "./values";1011export type DbRow = typeof schema.databases.$inferSelect;12const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;13export const DEFAULT_STATUS: SelectOption[] = [{ id: "todo", name: "Not started", color: "gray", group: "todo" }, { id: "doing", name: "In progress", color: "blue", group: "in_progress" }, { id: "done", name: "Done", color: "green", group: "done" }];1415/** Find a database by its id OR its host page id; create it lazily for a page of type "database". */16export async function ensureDatabase(wsId: string, id: string): Promise<DbRow> {17  if (!UUID.test(id)) throw new HttpError(400, "Invalid id");18  const [d] = await db.select().from(schema.databases).where(and(eq(schema.databases.workspaceId, wsId), or(eq(schema.databases.id, id), eq(schema.databases.pageId, id)), isNull(schema.databases.deletedAt)));19  if (d) return d;20  const [page] = await db.select().from(schema.pages).where(and(eq(schema.pages.id, id), eq(schema.pages.workspaceId, wsId), isNull(schema.pages.deletedAt)));21  if (!page || page.type !== "database") throw new HttpError(404, "Database not found");22  const [created] = await db.insert(schema.databases).values({ workspaceId: wsId, pageId: page.id, name: page.title, icon: page.icon }).returning();23  await db.insert(schema.databaseProperties).values({ databaseId: created.id, name: "Name", type: "title", position: 0, width: 260 });24  await db.insert(schema.databaseViews).values({ databaseId: created.id, name: "Table", type: "table", isDefault: true, position: 0 });25  return created;26}27export async function getDatabase(wsId: string, id: string): Promise<Database> {28  const d = await ensureDatabase(wsId, id);29  const [[page], properties, views] = await Promise.all([db.select({ title: schema.pages.title, icon: schema.pages.icon }).from(schema.pages).where(eq(schema.pages.id, d.pageId)), loadProps(d.id), db.select().from(schema.databaseViews).where(eq(schema.databaseViews.databaseId, d.id)).orderBy(schema.databaseViews.position, schema.databaseViews.createdAt)]);30  if (page && (page.title !== d.name || page.icon !== d.icon)) await db.update(schema.databases).set({ name: page.title, icon: page.icon }).where(eq(schema.databases.id, d.id));31  return { id: d.id, pageId: d.pageId, name: page?.title ?? d.name, description: d.description, icon: page?.icon ?? d.icon, isTemplate: d.isTemplate, templateOf: d.templateOf, properties, views: views as View[] };32}33export async function listDatabases(wsId: string) {34  const rows = await db.select({ id: schema.databases.id, pageId: schema.databases.pageId, name: schema.pages.title, icon: schema.pages.icon, isTemplate: schema.databases.isTemplate, templateOf: schema.databases.templateOf, updatedAt: schema.pages.updatedAt, rowCount: dsql<number>`(select count(*) from database_rows r where r.database_id = ${schema.databases.id} and r.deleted_at is null)` })35    .from(schema.databases).innerJoin(schema.pages, eq(schema.pages.id, schema.databases.pageId)).where(and(eq(schema.databases.workspaceId, wsId), isNull(schema.databases.deletedAt), isNull(schema.pages.deletedAt))).orderBy(schema.pages.title);36  return rows.map((r) => ({ ...r, rowCount: Number(r.rowCount) }));37}38export async function updateDatabase(wsId: string, id: string, patch: Partial<{ name: string; description: string | null; icon: string | null; isTemplate: boolean }>) {39  const d = await ensureDatabase(wsId, id);40  const set: Partial<DbRow> = { updatedAt: new Date() }; for (const k of ["name", "description", "icon", "isTemplate"] as const) if (patch[k] !== undefined) (set as any)[k] = patch[k];41  await db.update(schema.databases).set(set).where(eq(schema.databases.id, d.id));42  if (patch.name !== undefined || patch.icon !== undefined) await db.update(schema.pages).set({ ...(patch.name !== undefined ? { title: patch.name.trim() || "Untitled" } : {}), ...(patch.icon !== undefined ? { icon: patch.icon } : {}), updatedAt: new Date() }).where(eq(schema.pages.id, d.pageId));43  return getDatabase(wsId, d.id);44}4546// ---------- properties47function normalizeConfig(type: PropertyType, config: PropertyConfig = {}): PropertyConfig {48  const c: PropertyConfig = { ...config };49  if (type === "status" && !c.options?.length) c.options = DEFAULT_STATUS.map((o) => ({ ...o }));50  if ((type === "select" || type === "multi_select") && !c.options) c.options = [];51  if (c.options) c.options = c.options.map((o, i) => ({ id: o.id || newId(), name: String(o.name ?? `Option ${i + 1}`).trim() || `Option ${i + 1}`, color: o.color || "gray", ...(o.group ? { group: o.group } : {}) }));52  return c;53}54export async function createProperty(wsId: string, dbId: string, input: { name: string; type: PropertyType; config?: PropertyConfig; width?: number | null; position?: number }): Promise<Property> {55  const d = await ensureDatabase(wsId, dbId);56  if (!PROPERTY_TYPES.includes(input.type)) throw new HttpError(400, "Unknown property type");57  if (input.type === "title") throw new HttpError(400, "A database has exactly one title property");58  const [{ max }] = await db.select({ max: dsql<number>`coalesce(max(position),0)` }).from(schema.databaseProperties).where(eq(schema.databaseProperties.databaseId, d.id));59  const config = normalizeConfig(input.type, input.config);60  if (input.type === "relation" && config.targetDatabaseId) await ensureDatabase(wsId, config.targetDatabaseId);61  const [p] = await db.insert(schema.databaseProperties).values({ databaseId: d.id, name: input.name.trim() || "Property", type: input.type, config, width: input.width ?? (input.type === "checkbox" ? 90 : 180), position: input.position ?? Number(max) + 1 }).returning();62  if (input.type === "relation" && config.twoWay && config.targetDatabaseId) {63    const target = await ensureDatabase(wsId, config.targetDatabaseId);64    const [{ tmax }] = await db.select({ tmax: dsql<number>`coalesce(max(position),0)` }).from(schema.databaseProperties).where(eq(schema.databaseProperties.databaseId, target.id));65    await db.insert(schema.databaseProperties).values({ databaseId: target.id, name: target.id === d.id ? `${p.name} (reverse)` : d.name, type: "relation", config: { targetDatabaseId: d.id, syncedPropertyId: p.id }, position: Number(tmax) + 1 });66  }67  return p as Property;68}69export async function updateProperty(wsId: string, dbId: string, propId: string, patch: Partial<{ name: string; type: PropertyType; config: PropertyConfig; width: number | null; position: number }>): Promise<Property> {70  const d = await ensureDatabase(wsId, dbId);71  const [prev] = await db.select().from(schema.databaseProperties).where(and(eq(schema.databaseProperties.id, propId), eq(schema.databaseProperties.databaseId, d.id))); if (!prev) throw new HttpError(404, "Property not found");72  const set: Record<string, unknown> = {};73  if (patch.name !== undefined) set.name = patch.name.trim() || prev.name;74  if (patch.width !== undefined) set.width = patch.width; if (patch.position !== undefined) set.position = patch.position;75  const type = patch.type ?? (prev.type as PropertyType);76  if (patch.type && patch.type !== prev.type) {77    if (prev.type === "title" || patch.type === "title") throw new HttpError(400, "The title property cannot change type");78    if (!PROPERTY_TYPES.includes(patch.type)) throw new HttpError(400, "Unknown property type");79    set.type = patch.type;80  }81  if (patch.config !== undefined || set.type) set.config = normalizeConfig(type, { ...(patch.config ?? (set.type ? {} : prev.config)) });82  if (set.type && set.type !== "relation" && prev.type === "relation") await deleteRelationPair(prev as Property, d.id, true);83  if (prev.type === "relation" && type === "relation" && patch.config && patch.config.targetDatabaseId && patch.config.targetDatabaseId !== prev.config.targetDatabaseId) { await db.delete(schema.relationsTable).where(eq(schema.relationsTable.propertyId, prev.id)); }84  const [p] = await db.update(schema.databaseProperties).set(set).where(eq(schema.databaseProperties.id, propId)).returning();85  if (set.type && set.type !== prev.type) await convertValues(d.id, prev as Property, p as Property);86  return p as Property;87}88async function deleteRelationPair(p: Property, dbId: string, keepSelf: boolean) {89  const synced = p.config.syncedPropertyId as string | undefined;90  if (synced) { if (!keepSelf) await db.delete(schema.databaseProperties).where(eq(schema.databaseProperties.id, p.id)); return; }91  await db.delete(schema.databaseProperties).where(dsql`${schema.databaseProperties.config}->>'syncedPropertyId' = ${p.id}`);92  await db.delete(schema.relationsTable).where(eq(schema.relationsTable.propertyId, p.id));93  if (!keepSelf) await db.delete(schema.databaseProperties).where(eq(schema.databaseProperties.id, p.id));94}95export async function deleteProperty(wsId: string, dbId: string, propId: string) {96  const d = await ensureDatabase(wsId, dbId);97  const [p] = await db.select().from(schema.databaseProperties).where(and(eq(schema.databaseProperties.id, propId), eq(schema.databaseProperties.databaseId, d.id))); if (!p) throw new HttpError(404, "Property not found");98  if (p.type === "title") throw new HttpError(400, "The title property cannot be deleted");99  if (p.type === "relation") await deleteRelationPair(p as Property, d.id, false); else await db.delete(schema.databaseProperties).where(eq(schema.databaseProperties.id, propId));100  await db.update(schema.databaseRows).set({ values: dsql`${schema.databaseRows.values} - ${propId}` }).where(eq(schema.databaseRows.databaseId, d.id));101}102export async function reorderProperties(wsId: string, dbId: string, ids: string[]) {103  const d = await ensureDatabase(wsId, dbId);104  for (let i = 0; i < ids.length; i++) await db.update(schema.databaseProperties).set({ position: i + 1 }).where(and(eq(schema.databaseProperties.id, ids[i]), eq(schema.databaseProperties.databaseId, d.id)));105}106/** Best-effort conversion of stored values when a property changes type. */107async function convertValues(dbId: string, prev: Property, next: Property) {108  const raws = await loadRaw([eq(schema.databaseRows.databaseId, dbId)]); const opts = (next.config.options ??= []); let optsChanged = false;109  for (const r of raws) {110    const v = r.values[prev.id]; if (v == null) continue; let nv: unknown = null; const s = displayValue(prev, v);111    if (next.type === "text" || next.type === "url" || next.type === "ai") nv = s || null;112    else if (next.type === "number") nv = numberOf(typeof v === "number" ? v : s);113    else if (next.type === "checkbox") nv = Boolean(v) && v !== "false";114    else if (next.type === "date") nv = typeof v === "string" && !Number.isNaN(Date.parse(v)) ? v : null;115    else if (next.type === "select" || next.type === "status") { if (s) { const o = ensureOption(next, s.split(",")[0].trim()); nv = o.id; optsChanged = true; } }116    else if (next.type === "multi_select") { nv = s.split(",").map((x) => x.trim()).filter(Boolean).map((x) => { optsChanged = true; return ensureOption(next, x).id; }); }117    await db.update(schema.databaseRows).set({ values: nv == null ? dsql`${schema.databaseRows.values} - ${prev.id}` : dsql`${schema.databaseRows.values} || ${JSON.stringify({ [prev.id]: nv })}::jsonb` }).where(eq(schema.databaseRows.id, r.id));118  }119  if (optsChanged) await db.update(schema.databaseProperties).set({ config: { ...next.config, options: opts } }).where(eq(schema.databaseProperties.id, next.id));120}121122// ---------- rows123async function propsOf(d: DbRow) { return loadProps(d.id); }124const relIds = (v: unknown): string[] => (Array.isArray(v) ? v.map((x) => (typeof x === "string" ? x : (x as RelationRef)?.rowId)).filter((x): x is string => typeof x === "string" && UUID.test(x)) : typeof v === "string" && UUID.test(v) ? [v] : []);125export async function setRelations(p: Property, rowId: string, targetIds: string[]) {126  const synced = p.config.syncedPropertyId as string | undefined; const ids = [...new Set(targetIds)]; const single = p.config.single ? ids.slice(0, 1) : ids;127  if (synced) { await db.delete(schema.relationsTable).where(and(eq(schema.relationsTable.propertyId, synced), eq(schema.relationsTable.toRowId, rowId))); if (single.length) await db.insert(schema.relationsTable).values(single.map((f) => ({ propertyId: synced, fromRowId: f, toRowId: rowId }))).onConflictDoNothing(); }128  else { await db.delete(schema.relationsTable).where(and(eq(schema.relationsTable.propertyId, p.id), eq(schema.relationsTable.fromRowId, rowId))); if (single.length) await db.insert(schema.relationsTable).values(single.map((t) => ({ propertyId: p.id, fromRowId: rowId, toRowId: t }))).onConflictDoNothing(); }129}130/** Split incoming values into stored jsonb, title, and relation writes. */131function splitValues(props: Property[], values: Record<string, unknown>) {132  const stored: Record<string, unknown> = {}; const drop: string[] = []; let title: string | undefined; const rels: { p: Property; ids: string[] }[] = [];133  for (const [k, v] of Object.entries(values)) {134    const p = props.find((x) => x.id === k || x.name === k); if (!p) continue;135    if (p.type === "title") { title = v == null ? "" : String(v); stored[p.id] = title; continue; }136    if (p.type === "relation") { rels.push({ p, ids: relIds(v) }); continue; }137    if (COMPUTED_TYPES.has(p.type)) continue;138    if (v == null || v === "" || (Array.isArray(v) && !v.length)) drop.push(p.id); else stored[p.id] = v;139  }140  return { stored, drop, title, rels };141}142export async function createRow(wsId: string, dbId: string, input: { title?: string; values?: Record<string, unknown>; position?: number; content?: any; icon?: string | null }): Promise<RowOut> {143  const d = await ensureDatabase(wsId, dbId); const props = await propsOf(d);144  const { stored, title, rels } = splitValues(props, input.values ?? {}); const tp = titleProp(props);145  const t = (input.title ?? title ?? "").trim(); if (tp) stored[tp.id] = t;146  const page = await createPage(wsId, { title: t, parentId: d.pageId, section: "databases", type: "page", databaseId: d.id, content: input.content, icon: input.icon ?? null });147  const [{ max }] = await db.select({ max: dsql<number>`coalesce(max(position),0)` }).from(schema.databaseRows).where(eq(schema.databaseRows.databaseId, d.id));148  const [row] = await db.insert(schema.databaseRows).values({ databaseId: d.id, pageId: page.id, values: stored, position: input.position ?? Number(max) + 1 }).returning();149  for (const r of rels) await setRelations(r.p, row.id, r.ids);150  return (await getRows(d, props, [row.id]))[0];151}152export async function updateRow(wsId: string, dbId: string, rowId: string, values: Record<string, unknown>): Promise<RowOut> {153  const d = await ensureDatabase(wsId, dbId); const props = await propsOf(d);154  const [row] = await db.select().from(schema.databaseRows).where(and(eq(schema.databaseRows.id, rowId), eq(schema.databaseRows.databaseId, d.id), isNull(schema.databaseRows.deletedAt))); if (!row) throw new HttpError(404, "Row not found");155  const { stored, drop, title, rels } = splitValues(props, values);156  const next = { ...row.values, ...stored }; for (const k of drop) delete next[k];157  await db.update(schema.databaseRows).set({ values: next, updatedAt: new Date() }).where(eq(schema.databaseRows.id, rowId));158  if (title !== undefined) await db.update(schema.pages).set({ title: title.trim() || "Untitled", updatedAt: new Date() }).where(eq(schema.pages.id, row.pageId));159  else await db.update(schema.pages).set({ updatedAt: new Date() }).where(eq(schema.pages.id, row.pageId));160  for (const r of rels) await setRelations(r.p, rowId, r.ids);161  return (await getRows(d, props, [rowId]))[0];162}163export async function getRows(d: DbRow, props: Property[], ids: string[]): Promise<RowOut[]> { return hydrate(props, await loadRaw([eq(schema.databaseRows.databaseId, d.id), inArray(schema.databaseRows.id, ids.length ? ids : ["00000000-0000-0000-0000-000000000000"])])); }164export async function reorderRows(wsId: string, dbId: string, ids: string[]) {165  const d = await ensureDatabase(wsId, dbId);166  for (let i = 0; i < ids.length; i++) await db.update(schema.databaseRows).set({ position: i + 1 }).where(and(eq(schema.databaseRows.id, ids[i]), eq(schema.databaseRows.databaseId, d.id)));167}168export async function deleteRows(wsId: string, dbId: string, ids: string[]) {169  const d = await ensureDatabase(wsId, dbId); if (!ids.length) return 0;170  const rows = await db.select({ id: schema.databaseRows.id, pageId: schema.databaseRows.pageId }).from(schema.databaseRows).where(and(eq(schema.databaseRows.databaseId, d.id), inArray(schema.databaseRows.id, ids)));171  if (!rows.length) return 0;172  await db.update(schema.databaseRows).set({ deletedAt: new Date() }).where(inArray(schema.databaseRows.id, rows.map((r) => r.id)));173  for (const r of rows) await softDeletePage(wsId, r.pageId).catch(() => {});174  return rows.length;175}176export async function listRowsSimple(wsId: string, dbId: string, q: string | undefined, limit = 50) {177  const d = await ensureDatabase(wsId, dbId);178  const raws = await loadRaw([eq(schema.databaseRows.databaseId, d.id), ...(q?.trim() ? [dsql`${schema.pages.title} ilike ${"%" + q.trim().replace(/[%_\\]/g, "\\$&") + "%"}`] : [])]);179  return raws.slice(0, limit).map((r): RelationRef => ({ rowId: r.id, pageId: r.pageId, title: r.title, icon: r.icon }));180}181export async function enqueueAi(wsId: string, dbId: string, rowId: string, propertyId: string) {182  const d = await ensureDatabase(wsId, dbId); const props = await propsOf(d); const p = props.find((x) => x.id === propertyId);183  if (!p || p.type !== "ai") throw new HttpError(400, "Not an AI property");184  await db.update(schema.databaseRows).set({ values: dsql`${schema.databaseRows.values} || ${JSON.stringify({ [p.id]: { pending: true, at: new Date().toISOString() } })}::jsonb` }).where(and(eq(schema.databaseRows.id, rowId), eq(schema.databaseRows.databaseId, d.id)));185  await enqueue(QUEUES.aiProperty, { rowId, propertyId, workspaceId: wsId }, { singletonKey: `ai:${rowId}:${propertyId}`, singletonSeconds: 5 });186}187188// ---------- views189export async function createView(wsId: string, dbId: string, input: { name: string; type: ViewType; config?: ViewConfig }): Promise<View> {190  const d = await ensureDatabase(wsId, dbId); if (!VIEW_TYPES.includes(input.type)) throw new HttpError(400, "Unknown view type");191  const [{ max }] = await db.select({ max: dsql<number>`coalesce(max(position),0)` }).from(schema.databaseViews).where(eq(schema.databaseViews.databaseId, d.id));192  const [v] = await db.insert(schema.databaseViews).values({ databaseId: d.id, name: input.name.trim() || input.type, type: input.type, config: input.config ?? {}, position: Number(max) + 1 }).returning(); return v as View;193}194export async function updateView(wsId: string, dbId: string, viewId: string, patch: Partial<{ name: string; type: ViewType; config: ViewConfig; position: number; isDefault: boolean }>): Promise<View> {195  const d = await ensureDatabase(wsId, dbId);196  const [prev] = await db.select().from(schema.databaseViews).where(and(eq(schema.databaseViews.id, viewId), eq(schema.databaseViews.databaseId, d.id))); if (!prev) throw new HttpError(404, "View not found");197  if (patch.isDefault) await db.update(schema.databaseViews).set({ isDefault: false }).where(eq(schema.databaseViews.databaseId, d.id));198  const set: Record<string, unknown> = {}; if (patch.name !== undefined) set.name = patch.name.trim() || prev.name; if (patch.type) set.type = patch.type; if (patch.config !== undefined) set.config = { ...(prev.config as ViewConfig), ...patch.config }; if (patch.position !== undefined) set.position = patch.position; if (patch.isDefault !== undefined) set.isDefault = patch.isDefault;199  const [v] = await db.update(schema.databaseViews).set(set).where(eq(schema.databaseViews.id, viewId)).returning(); return v as View;200}201export async function deleteView(wsId: string, dbId: string, viewId: string) {202  const d = await ensureDatabase(wsId, dbId);203  const all = await db.select({ id: schema.databaseViews.id }).from(schema.databaseViews).where(eq(schema.databaseViews.databaseId, d.id)); if (all.length <= 1) throw new HttpError(400, "A database needs at least one view");204  await db.delete(schema.databaseViews).where(and(eq(schema.databaseViews.id, viewId), eq(schema.databaseViews.databaseId, d.id)));205}206207// ---------- duplicate (templates)208const remapConfig = (c: Record<string, unknown>, map: Map<string, string>): Record<string, unknown> => JSON.parse(JSON.stringify(c), (k, v) => (typeof v === "string" && map.has(v) ? map.get(v) : v));209export async function duplicateDatabase(wsId: string, dbId: string, opts: { withRows?: boolean; title?: string; asTemplate?: boolean; parentId?: string | null } = {}) {210  const src = await getDatabase(wsId, dbId);211  const page = await createPage(wsId, { title: opts.title ?? `${src.name} (copy)`, type: "database", section: "databases", icon: src.icon, parentId: opts.parentId ?? null });212  const [d] = await db.insert(schema.databases).values({ workspaceId: wsId, pageId: page.id, name: page.title, icon: src.icon, description: src.description, isTemplate: opts.asTemplate ?? false, templateOf: src.isTemplate ? src.id : null }).returning();213  const pmap = new Map<string, string>();214  for (const p of src.properties) { const [np] = await db.insert(schema.databaseProperties).values({ databaseId: d.id, name: p.name, type: p.type, config: p.config, position: p.position, width: p.width }).returning(); pmap.set(p.id, np.id); }215  pmap.set(src.id, d.id);216  for (const id of pmap.keys()) if (id !== src.id) { const p = src.properties.find((x) => x.id === id)!; const c = remapConfig(p.config, pmap); if (p.type === "relation") { delete c.syncedPropertyId; delete c.twoWay; } if (JSON.stringify(c) !== JSON.stringify(p.config)) await db.update(schema.databaseProperties).set({ config: c }).where(eq(schema.databaseProperties.id, pmap.get(id)!)); }217  for (const v of src.views) await db.insert(schema.databaseViews).values({ databaseId: d.id, name: v.name, type: v.type, config: remapConfig(v.config, pmap), position: v.position, isDefault: v.isDefault });218  if (opts.withRows) {219    const raws = await loadRaw([eq(schema.databaseRows.databaseId, src.id)]); const rmap = new Map<string, string>();220    const pagesById = new Map((await db.select({ id: schema.pages.id, content: schema.pages.content }).from(schema.pages).where(inArray(schema.pages.id, raws.length ? raws.map((r) => r.pageId) : ["00000000-0000-0000-0000-000000000000"]))).map((p) => [p.id, p.content]));221    for (const r of raws) {222      const np = await createPage(wsId, { title: r.title, parentId: page.id, section: "databases", databaseId: d.id, icon: r.icon, content: pagesById.get(r.pageId) as any });223      const values: Record<string, unknown> = {}; for (const [k, v] of Object.entries(r.values)) if (pmap.has(k)) values[pmap.get(k)!] = v;224      const [nr] = await db.insert(schema.databaseRows).values({ databaseId: d.id, pageId: np.id, values, position: r.position }).returning(); rmap.set(r.id, nr.id);225    }226    const relProps = src.properties.filter((p) => p.type === "relation" && !p.config.syncedPropertyId);227    if (relProps.length && raws.length) {228      const rels = await db.select().from(schema.relationsTable).where(and(inArray(schema.relationsTable.propertyId, relProps.map((p) => p.id)), inArray(schema.relationsTable.fromRowId, raws.map((r) => r.id))));229      const self = (pid: string) => relProps.find((p) => p.id === pid)?.config.targetDatabaseId === src.id;230      const vals = rels.map((x) => ({ propertyId: pmap.get(x.propertyId)!, fromRowId: rmap.get(x.fromRowId)!, toRowId: self(x.propertyId) ? rmap.get(x.toRowId) : x.toRowId })).filter((x) => x.toRowId) as { propertyId: string; fromRowId: string; toRowId: string }[];231      if (vals.length) await db.insert(schema.relationsTable).values(vals).onConflictDoNothing();232    }233  }234  return getDatabase(wsId, d.id);235}236export { queryRows, isEmpty };237
Files · main
Menu

Theme