one directory per feature: schema.ts and queries.ts together
src/databases/officer_db/src/<feature>/{schema.ts,queries.ts}, replacing the
parallel schema/ and queries/ trees. 24 feature directories, 46 files moved with
git mv so history follows.
The parallel trees had drifted, which is what the restructure is really fixing:
four features were named differently on each side — app-store/sidecar-installs,
email/email-accounts, server/server-config
operations had a schema and NO query file: its task_logs is reached directly
from src/servers/api/task-logger.ts, bypassing this package's own boundary
integrations had queries and NO schema, because it spans two features'
tables — server_integrations and user_integrations
Both lopsided cases survive as directories holding one file, which states the
problem instead of hiding it across two trees.
Nothing outside the package changed how it imports. `officerdb`, `officerdb/types`
and `officerdb/db` resolve exactly as before; index.ts absorbed the path changes.
Added `"./*": "./src/*"` so the new layout is reachable — `officerdb/soulseek/schema`
— which one script needed, because soulseek is a plugin and therefore commented
out of the aggregator.
schema/index.ts became src/schema.ts, keeping the core/plugin split from earlier
tonight. drizzle.config.ts and the package's "./schema" export follow it.
Verified rather than assumed: all 52 files in the package parse, every relative
import resolves against the new layout (checked by walking each specifier to a
real file, since parsing does not check paths), and everything in the tree
importing officerdb still parses. Not typechecked — empty node_modules, frozen
installs.
One rewrite bug worth recording: the rule mapping a query module's sibling import
also matched the './schema' this pass had just written, turning it into
'../schema/queries' in 22 files. Caught by the resolver check, not by parsing —
both spellings parse fine.
Also corrects every path reference the move invalidated: src/databases/CLAUDE.md's
layout diagram, the root CLAUDE.md data section, three docs, and seven sidecar
comments naming queries/<x>.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,8 +123,9 @@ imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helper
|
|||||||
Two stores, and the split matters:
|
Two stores, and the split matters:
|
||||||
|
|
||||||
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
|
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
|
||||||
email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in
|
email accounts, queue and pipeline jobs. One directory per feature holding `schema.ts` and
|
||||||
`src/queries/`, types inferred from the schema in `src/types.ts`.
|
`queries.ts` beside each other; `src/schema.ts` is what `db:push` reads, and it lists the core tables
|
||||||
|
with the plugin ones commented out. Types inferred from the schema in `src/types.ts`.
|
||||||
|
|
||||||
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
|
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
|
||||||
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
|
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
|
||||||
|
|||||||
@@ -327,4 +327,4 @@ service verbs exist (`listApiKeys`, `revokeApiKey`) if that changes.
|
|||||||
bearer string into a caller. All four doors call it: `userMiddleware`, `originScopeMiddleware`, the
|
bearer string into a caller. All four doors call it: `userMiddleware`, `originScopeMiddleware`, the
|
||||||
WebSocket upgrade in `server.tsx`, and the vault socket.
|
WebSocket upgrade in `server.tsx`, and the vault socket.
|
||||||
- `src/servers/api/api-keys/router.ts` — the three endpoints.
|
- `src/servers/api/api-keys/router.ts` — the three endpoints.
|
||||||
- `src/databases/officer_db/src/schema/api-keys.ts` — the table, and why it stores what it stores.
|
- `src/databases/officer_db/src/api-keys/schema.ts` — the table, and why it stores what it stores.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ does and does not protect against.
|
|||||||
|
|
||||||
Authoritative for the crypto design. The code is `src/servers/sidecar/wallet/keys.ts` (sealing,
|
Authoritative for the crypto design. The code is `src/servers/sidecar/wallet/keys.ts` (sealing,
|
||||||
derivation, unlock sessions), `src/databases/officer_db/src/crypto.ts` (storage encryption) and
|
derivation, unlock sessions), `src/databases/officer_db/src/crypto.ts` (storage encryption) and
|
||||||
`src/databases/officer_db/src/queries/wallet.ts` (where the two meet).
|
`src/databases/officer_db/src/wallet/queries.ts` (where the two meet).
|
||||||
|
|
||||||
## The requirement
|
## The requirement
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ and the rename sequence leaves `workspaces` with no zombie.
|
|||||||
- [x] **`ws-terminals-{id}: null` on a live dashboard is a 500.** Same file, `:61-66` — the
|
- [x] **`ws-terminals-{id}: null` on a live dashboard is a 500.** Same file, `:61-66` — the
|
||||||
`ws-layout-*` branch has a `value === null` → `deleteDashboard` case (`:42`); the terminals
|
`ws-layout-*` branch has a `value === null` → `deleteDashboard` case (`:42`); the terminals
|
||||||
branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column
|
branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column
|
||||||
(`databases/officer_db/src/queries/dashboards.ts:70`) → 23502.
|
(`databases/officer_db/src/dashboards/queries.ts:70`) → 23502.
|
||||||
**Resolved.** A null on either terminals branch is now a no-op: it means "forget this key", and it
|
**Resolved.** A null on either terminals branch is now a no-op: it means "forget this key", and it
|
||||||
only ever arrives paired with `ws-layout-{id}: null` on a rename, by which point the row is gone.
|
only ever arrives paired with `ws-layout-{id}: null` on a rename, by which point the row is gone.
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ these.
|
|||||||
> which uuid ids would not.
|
> which uuid ids would not.
|
||||||
|
|
||||||
- [ ] **`dashboards.id` is a global primary key but ids are `slugify(name)`.**
|
- [ ] **`dashboards.id` is a global primary key but ids are `slugify(name)`.**
|
||||||
`databases/officer_db/src/schema/dashboards.ts` declares `id: text('id').primaryKey()`. Live:
|
`databases/officer_db/src/dashboards/schema.ts` declares `id: text('id').primaryKey()`. Live:
|
||||||
`"dashboards_pkey" PRIMARY KEY, btree (id)` plus a redundant
|
`"dashboards_pkey" PRIMARY KEY, btree (id)` plus a redundant
|
||||||
`"uq_dashboards_user_id" UNIQUE, btree (user_id, id)` — evidence per-user ids were intended and
|
`"uq_dashboards_user_id" UNIQUE, btree (user_id, id)` — evidence per-user ids were intended and
|
||||||
half-built. Ids come from `DashboardPreview.tsx:300` (`slugify(trimmed) || generateSlug()`) and the
|
half-built. Ids come from `DashboardPreview.tsx:300` (`slugify(trimmed) || generateSlug()`) and the
|
||||||
@@ -213,7 +213,7 @@ these.
|
|||||||
(see `databases/CLAUDE.md` → "Composite keys") — harmless churn, but read the plan.
|
(see `databases/CLAUDE.md` → "Composite keys") — harmless churn, but read the plan.
|
||||||
|
|
||||||
- [x] **`upsertDashboard`'s UPDATE has no `userId` predicate.**
|
- [x] **`upsertDashboard`'s UPDATE has no `userId` predicate.**
|
||||||
`databases/officer_db/src/queries/dashboards.ts:73` —
|
`databases/officer_db/src/dashboards/queries.ts:73` —
|
||||||
`db.update(dashboards).set(set).where(eq(dashboards.id, id))`. The `existing` lookup above it _is_
|
`db.update(dashboards).set(set).where(eq(dashboards.id, id))`. The `existing` lookup above it _is_
|
||||||
scoped, so it cannot reach another user's row today, but it is a non-transactional read-then-write.
|
scoped, so it cannot reach another user's row today, but it is a non-transactional read-then-write.
|
||||||
**It becomes a live cross-user overwrite the moment the PK above is made composite.**
|
**It becomes a live cross-user overwrite the moment the PK above is made composite.**
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
import type { BrowsedFile } from 'officerdb';
|
import type { BrowsedFile } from 'officerdb';
|
||||||
import { eq, asc } from 'drizzle-orm';
|
import { eq, asc } from 'drizzle-orm';
|
||||||
import { db, finishSoulseekBrowse } from 'officerdb';
|
import { db, finishSoulseekBrowse } from 'officerdb';
|
||||||
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/schema';
|
// soulseek is a plugin, so its tables are commented out of officerdb's schema aggregator —
|
||||||
|
// import them from the feature directly.
|
||||||
|
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/soulseek/schema';
|
||||||
import { buildTree } from '../src/servers/sidecar/slskd/browse';
|
import { buildTree } from '../src/servers/sidecar/slskd/browse';
|
||||||
|
|
||||||
const snapshots = await db
|
const snapshots = await db
|
||||||
|
|||||||
+25
-12
@@ -9,18 +9,31 @@ describing a different codebase.
|
|||||||
```
|
```
|
||||||
src/databases/officer_db/
|
src/databases/officer_db/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── db.ts # the connection
|
│ ├── db.ts # the connection
|
||||||
│ ├── index.ts # public surface: re-exports queries, schema and drizzle helpers
|
│ ├── index.ts # public surface: re-exports every feature's queries
|
||||||
│ ├── types.ts # every type export (Select / Insert / extended)
|
│ ├── schema.ts # what db:push creates — see below
|
||||||
│ └── schema/
|
│ ├── types.ts # every type export (Select / Insert / extended)
|
||||||
│ ├── index.ts # re-exports all schema files
|
│ ├── crypto.ts # at-rest encryption, one key per purpose
|
||||||
│ └── *.ts # table definitions, grouped by domain
|
│ ├── secret-store.ts # the key store itself (SQLite, outside Postgres)
|
||||||
└── package.json # exports "." and "./types"
|
│ └── <feature>/
|
||||||
|
│ ├── schema.ts # its tables
|
||||||
|
│ └── queries.ts # everything that reads or writes them
|
||||||
|
└── package.json # exports ".", "./types", "./db", "./schema", "./secret-store", "./*"
|
||||||
```
|
```
|
||||||
|
|
||||||
Schema files are grouped by domain, not by table: `auth`, `chat-events`, `dashboards`, `email`,
|
**One directory per feature, holding both halves.** Restructured 2026-08-13 from parallel `schema/` and
|
||||||
`headscale`, `music`, `operations`, `pipeline-jobs`, `server`, `soulseek`, `user-data`, `vault`,
|
`queries/` trees, where the two sides had drifted: four features were named differently on each side
|
||||||
`wallet`.
|
(`app-store`/`sidecar-installs`, `email`/`email-accounts`, `server`/`server-config`), `operations` had no
|
||||||
|
query file at all, and `integrations` had no schema file.
|
||||||
|
|
||||||
|
Two directories are still lopsided and say so by their contents: `operations/` has only a schema (its
|
||||||
|
`task_logs` is reached directly from `src/servers/`, bypassing this package), and `integrations/` has only
|
||||||
|
queries, because it spans `server` and `user-data`.
|
||||||
|
|
||||||
|
**`src/schema.ts` is drizzle-kit's view, not the runtime's.** `drizzle.config.ts` points at it, so a
|
||||||
|
commented line there removes a table from the DATABASE without removing a line of code — every query
|
||||||
|
imports its tables from `./schema` inside its own feature directory. That is what lets a fresh install
|
||||||
|
create only the core tables, with the plugin ones commented out until their plugin is installed.
|
||||||
|
|
||||||
## Schema changes use `push`, not migrations
|
## Schema changes use `push`, not migrations
|
||||||
|
|
||||||
@@ -114,8 +127,8 @@ Organise `types.ts` by domain with section comments, mirroring the schema files.
|
|||||||
|
|
||||||
## Queries
|
## Queries
|
||||||
|
|
||||||
Hand-written, one file per domain under `src/queries/`, importing tables from `../schema` and types from
|
Hand-written, one `queries.ts` per feature directory, importing tables from `./schema` beside it and
|
||||||
`../types`:
|
types from `../types`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ try {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './src/schema/index.ts',
|
schema: './src/schema.ts',
|
||||||
out: './migrations',
|
out: './migrations',
|
||||||
dialect: 'postgresql',
|
dialect: 'postgresql',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./types": "./src/types.ts",
|
"./types": "./src/types.ts",
|
||||||
"./db": "./src/db.ts",
|
"./db": "./src/db.ts",
|
||||||
"./schema": "./src/schema/index.ts",
|
"./schema": "./src/schema.ts",
|
||||||
"./secret-store": "./src/secret-store.ts"
|
"./secret-store": "./src/secret-store.ts",
|
||||||
|
"./*": "./src/*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "drizzle-kit generate --config=drizzle.config.ts",
|
"generate": "drizzle-kit generate --config=drizzle.config.ts",
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { and, asc, eq } from 'drizzle-orm';
|
import { and, asc, eq } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { agentPanels } from '../schema';
|
import { agentPanels } from './schema';
|
||||||
import type { AgentPanelRow } from '../schema/agent-panels';
|
import type { AgentPanelRow } from './schema';
|
||||||
|
|
||||||
export type AgentPanel = AgentPanelRow;
|
export type AgentPanel = AgentPanelRow;
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, text, integer, timestamp, uniqueIndex, index } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, timestamp, uniqueIndex, index } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One named agent living in one dashboard panel — the address book that lets two chat panels on the
|
* One named agent living in one dashboard panel — the address book that lets two chat panels on the
|
||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { eq, and, isNull, sql } from 'drizzle-orm';
|
import { eq, and, isNull, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { apiKeys, users } from '../schema';
|
import { apiKeys } from './schema';
|
||||||
|
import { users } from '../auth/schema';
|
||||||
import type { ApiKeySelect } from '../types';
|
import type { ApiKeySelect } from '../types';
|
||||||
|
|
||||||
// Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is
|
// Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password.
|
// Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { sidecarInstalls } from '../schema';
|
import { sidecarInstalls } from './schema';
|
||||||
|
|
||||||
// What the owner has installed from the app store. See ../schema/app-store.ts for why there is no
|
// What the owner has installed from the app store. See ../schema/app-store.ts for why there is no
|
||||||
// userId and why `installed` and `enabled` are separate.
|
// userId and why `installed` and `enabled` are separate.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, lt, sql } from 'drizzle-orm';
|
import { eq, and, lt, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from '../schema';
|
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from './schema';
|
||||||
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';
|
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';
|
||||||
|
|
||||||
// ── Users ──
|
// ── Users ──
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { roleCapabilities } from '../schema';
|
import { roleCapabilities } from './schema';
|
||||||
import type { UserRole } from '../schema/auth';
|
import type { UserRole } from '../auth/schema';
|
||||||
import type { CapabilityLevelValue } from '../schema/capabilities';
|
import type { CapabilityLevelValue } from './schema';
|
||||||
|
|
||||||
// Grants, keyed on role. Absence denies — see the table comment.
|
// Grants, keyed on role. Absence denies — see the table comment.
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, text, timestamp, uniqueIndex, check } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, timestamp, uniqueIndex, check } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { USER_ROLES } from './auth';
|
import { USER_ROLES } from '../auth/schema';
|
||||||
|
|
||||||
// What a ROLE may reach. The subject of a grant is a role, never a user.
|
// What a ROLE may reach. The subject of a grant is a role, never a user.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, gt, asc, desc, lt } from 'drizzle-orm';
|
import { eq, and, gt, asc, desc, lt } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { chatSessionEvents } from '../schema';
|
import { chatSessionEvents } from './schema';
|
||||||
|
|
||||||
/** Append one outbound event to a session's durable log; returns its global cursor id. */
|
/** Append one outbound event to a session's durable log; returns its global cursor id. */
|
||||||
export async function appendChatEvent(sessionId: string, event: unknown): Promise<number> {
|
export async function appendChatEvent(sessionId: string, event: unknown): Promise<number> {
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { dashboards, screens, dashboardDefaults } from '../schema';
|
import { dashboards, screens, dashboardDefaults } from './schema';
|
||||||
|
|
||||||
// ── Full state read ──
|
// ── Full state read ──
|
||||||
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
|
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `LayoutNode` is an object, and the whole write path to these columns is `unknown` — the PATCH body is
|
* A `LayoutNode` is an object, and the whole write path to these columns is `unknown` — the PATCH body is
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { and, desc, eq, isNull } from 'drizzle-orm';
|
|||||||
import argon2 from 'argon2';
|
import argon2 from 'argon2';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { davAppPasswords } from '../schema/dav';
|
import { davAppPasswords } from './schema';
|
||||||
|
|
||||||
export type DavAppPassword = typeof davAppPasswords.$inferSelect;
|
export type DavAppPassword = typeof davAppPasswords.$inferSelect;
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, integer, text, timestamp, index } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, timestamp, index } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Per-device credentials for CalDAV / CardDAV clients — DAVx5, iOS, macOS, Thunderbird.
|
// Per-device credentials for CalDAV / CardDAV clients — DAVx5, iOS, macOS, Thunderbird.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { emailAccounts } from '../schema/email';
|
import { emailAccounts } from './schema';
|
||||||
import type { EmailAccountInsert, EmailAccountSelect } from '../types';
|
import type { EmailAccountInsert, EmailAccountSelect } from '../types';
|
||||||
|
|
||||||
export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> {
|
export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
export const emailAccounts = pgTable(
|
export const emailAccounts = pgTable(
|
||||||
'email_accounts',
|
'email_accounts',
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { headscaleServers } from '../schema';
|
import { headscaleServers } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Headscale server registry access for the officer-headscale sidecar. Callers deal in PLAINTEXT —
|
// Headscale server registry access for the officer-headscale sidecar. Callers deal in PLAINTEXT —
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single
|
// The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single
|
||||||
// Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and
|
// Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and
|
||||||
@@ -18,7 +18,7 @@ export {
|
|||||||
blacklistToken,
|
blacklistToken,
|
||||||
isTokenBlacklisted,
|
isTokenBlacklisted,
|
||||||
cleanupExpiredTokens,
|
cleanupExpiredTokens,
|
||||||
} from './queries/auth';
|
} from './auth/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
findLiveApiKeyByHash,
|
findLiveApiKeyByHash,
|
||||||
@@ -27,9 +27,9 @@ export {
|
|||||||
revokeApiKey,
|
revokeApiKey,
|
||||||
touchApiKey,
|
touchApiKey,
|
||||||
type ApiKeyIdentity,
|
type ApiKeyIdentity,
|
||||||
} from './queries/api-keys';
|
} from './api-keys/queries';
|
||||||
|
|
||||||
export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './queries/server-config';
|
export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './server/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getUserSettings,
|
getUserSettings,
|
||||||
@@ -38,7 +38,7 @@ export {
|
|||||||
patchUserState,
|
patchUserState,
|
||||||
getDockPaths,
|
getDockPaths,
|
||||||
setDockPaths,
|
setDockPaths,
|
||||||
} from './queries/user-data';
|
} from './user-data/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getServerIntegrations,
|
getServerIntegrations,
|
||||||
@@ -51,7 +51,7 @@ export {
|
|||||||
upsertUserIntegration,
|
upsertUserIntegration,
|
||||||
deleteUserIntegration,
|
deleteUserIntegration,
|
||||||
findUserByIntegrationConfig,
|
findUserByIntegrationConfig,
|
||||||
} from './queries/integrations';
|
} from './integrations/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getEmailAccounts,
|
getEmailAccounts,
|
||||||
@@ -61,7 +61,7 @@ export {
|
|||||||
updateEmailAccountStatus,
|
updateEmailAccountStatus,
|
||||||
updateEmailAccountSyncMeta,
|
updateEmailAccountSyncMeta,
|
||||||
getAllSyncedAccounts,
|
getAllSyncedAccounts,
|
||||||
} from './queries/email-accounts';
|
} from './email/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAllDashboardState,
|
getAllDashboardState,
|
||||||
@@ -73,7 +73,7 @@ export {
|
|||||||
deleteScreen,
|
deleteScreen,
|
||||||
upsertDefaults,
|
upsertDefaults,
|
||||||
setDefaultsPanelState,
|
setDefaultsPanelState,
|
||||||
} from './queries/dashboards';
|
} from './dashboards/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createPipelineJob,
|
createPipelineJob,
|
||||||
@@ -86,14 +86,14 @@ export {
|
|||||||
deletePipelineJob,
|
deletePipelineJob,
|
||||||
deleteTerminalJobsForUser,
|
deleteTerminalJobsForUser,
|
||||||
markInterruptedJobs,
|
markInterruptedJobs,
|
||||||
} from './queries/pipeline-jobs';
|
} from './pipeline-jobs/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
appendChatEvent,
|
appendChatEvent,
|
||||||
getChatEventsSince,
|
getChatEventsSince,
|
||||||
getLastChatEventSeq,
|
getLastChatEventSeq,
|
||||||
pruneChatEventsOlderThan,
|
pruneChatEventsOlderThan,
|
||||||
} from './queries/chat-events';
|
} from './chat-events/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
listAgentPanels,
|
listAgentPanels,
|
||||||
@@ -105,8 +105,8 @@ export {
|
|||||||
markAgentPanelIntroduced,
|
markAgentPanelIntroduced,
|
||||||
deleteAgentPanel,
|
deleteAgentPanel,
|
||||||
toAgentPanelView,
|
toAgentPanelView,
|
||||||
} from './queries/agent-panels';
|
} from './agent-panels/queries';
|
||||||
export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './queries/agent-panels';
|
export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './agent-panels/queries';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getMusicFavorites,
|
getMusicFavorites,
|
||||||
@@ -122,7 +122,7 @@ export {
|
|||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
addPlaylistItems,
|
addPlaylistItems,
|
||||||
setPlaylistItems,
|
setPlaylistItems,
|
||||||
} from './queries/music';
|
} from './music/queries';
|
||||||
export type {
|
export type {
|
||||||
FavoriteKind,
|
FavoriteKind,
|
||||||
GroupedFavorites,
|
GroupedFavorites,
|
||||||
@@ -130,8 +130,8 @@ export type {
|
|||||||
NowPlayingInput,
|
NowPlayingInput,
|
||||||
PlaylistSummary,
|
PlaylistSummary,
|
||||||
Playlist,
|
Playlist,
|
||||||
} from './queries/music';
|
} from './music/queries';
|
||||||
export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './queries/soulseek';
|
export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './soulseek/queries';
|
||||||
export {
|
export {
|
||||||
getSoulseekBrowseSnapshots,
|
getSoulseekBrowseSnapshots,
|
||||||
getSoulseekBrowseSnapshot,
|
getSoulseekBrowseSnapshot,
|
||||||
@@ -144,7 +144,7 @@ export {
|
|||||||
getSoulseekBrowseDirFiles,
|
getSoulseekBrowseDirFiles,
|
||||||
getSoulseekBrowseDownload,
|
getSoulseekBrowseDownload,
|
||||||
deleteSoulseekBrowse,
|
deleteSoulseekBrowse,
|
||||||
} from './queries/soulseek';
|
} from './soulseek/queries';
|
||||||
export type {
|
export type {
|
||||||
BrowseDownloadFile,
|
BrowseDownloadFile,
|
||||||
BrowsedFile,
|
BrowsedFile,
|
||||||
@@ -154,7 +154,7 @@ export type {
|
|||||||
BrowseLevel,
|
BrowseLevel,
|
||||||
BrowseTreeSearch,
|
BrowseTreeSearch,
|
||||||
SoulseekBrowseSnapshot,
|
SoulseekBrowseSnapshot,
|
||||||
} from './queries/soulseek';
|
} from './soulseek/queries';
|
||||||
export {
|
export {
|
||||||
listHeadscaleServers,
|
listHeadscaleServers,
|
||||||
getActiveHeadscaleCredentials,
|
getActiveHeadscaleCredentials,
|
||||||
@@ -164,8 +164,8 @@ export {
|
|||||||
setActiveHeadscaleServer,
|
setActiveHeadscaleServer,
|
||||||
deleteHeadscaleServer,
|
deleteHeadscaleServer,
|
||||||
recordHeadscaleProbe,
|
recordHeadscaleProbe,
|
||||||
} from './queries/headscale';
|
} from './headscale/queries';
|
||||||
export type { HeadscaleServer, HeadscaleServerCredentials } from './queries/headscale';
|
export type { HeadscaleServer, HeadscaleServerCredentials } from './headscale/queries';
|
||||||
export {
|
export {
|
||||||
listInvoiceshelfAccounts,
|
listInvoiceshelfAccounts,
|
||||||
getActiveInvoiceshelfCredentials,
|
getActiveInvoiceshelfCredentials,
|
||||||
@@ -175,8 +175,8 @@ export {
|
|||||||
setActiveInvoiceshelfAccount,
|
setActiveInvoiceshelfAccount,
|
||||||
deleteInvoiceshelfAccount,
|
deleteInvoiceshelfAccount,
|
||||||
recordInvoiceshelfProbe,
|
recordInvoiceshelfProbe,
|
||||||
} from './queries/invoiceshelf';
|
} from './invoiceshelf/queries';
|
||||||
export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './queries/invoiceshelf';
|
export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './invoiceshelf/queries';
|
||||||
export {
|
export {
|
||||||
listJellyfinServers,
|
listJellyfinServers,
|
||||||
getActiveJellyfinCredentials,
|
getActiveJellyfinCredentials,
|
||||||
@@ -186,8 +186,8 @@ export {
|
|||||||
setActiveJellyfinServer,
|
setActiveJellyfinServer,
|
||||||
deleteJellyfinServer,
|
deleteJellyfinServer,
|
||||||
recordJellyfinProbe,
|
recordJellyfinProbe,
|
||||||
} from './queries/jellyfin';
|
} from './jellyfin/queries';
|
||||||
export type { JellyfinServer, JellyfinCredentials } from './queries/jellyfin';
|
export type { JellyfinServer, JellyfinCredentials } from './jellyfin/queries';
|
||||||
export {
|
export {
|
||||||
listPhotosAccounts,
|
listPhotosAccounts,
|
||||||
getActivePhotosCredentials,
|
getActivePhotosCredentials,
|
||||||
@@ -197,16 +197,16 @@ export {
|
|||||||
setActivePhotosAccount,
|
setActivePhotosAccount,
|
||||||
deletePhotosAccount,
|
deletePhotosAccount,
|
||||||
recordPhotosProbe,
|
recordPhotosProbe,
|
||||||
} from './queries/photos';
|
} from './photos/queries';
|
||||||
export type { PhotosAccount, PhotosCredentials } from './queries/photos';
|
export type { PhotosAccount, PhotosCredentials } from './photos/queries';
|
||||||
export {
|
export {
|
||||||
listDavAppPasswords,
|
listDavAppPasswords,
|
||||||
createDavAppPassword,
|
createDavAppPassword,
|
||||||
revokeDavAppPassword,
|
revokeDavAppPassword,
|
||||||
deleteDavAppPassword,
|
deleteDavAppPassword,
|
||||||
verifyDavAppPassword,
|
verifyDavAppPassword,
|
||||||
} from './queries/dav';
|
} from './dav/queries';
|
||||||
export type { DavAppPassword, DavAppPasswordView } from './queries/dav';
|
export type { DavAppPassword, DavAppPasswordView } from './dav/queries';
|
||||||
export {
|
export {
|
||||||
getServiceConnection,
|
getServiceConnection,
|
||||||
getServiceCredentials,
|
getServiceCredentials,
|
||||||
@@ -215,17 +215,17 @@ export {
|
|||||||
recordServiceProbe,
|
recordServiceProbe,
|
||||||
getServiceInstanceUrl,
|
getServiceInstanceUrl,
|
||||||
getResolvedServiceCredentials,
|
getResolvedServiceCredentials,
|
||||||
} from './queries/service-connections';
|
} from './service-connections/queries';
|
||||||
export type { ServiceName, ServiceConnection, ServiceCredentials } from './queries/service-connections';
|
export type { ServiceName, ServiceConnection, ServiceCredentials } from './service-connections/queries';
|
||||||
export {
|
export {
|
||||||
getAllRoleGrants,
|
getAllRoleGrants,
|
||||||
getRoleGrants,
|
getRoleGrants,
|
||||||
setRoleGrant,
|
setRoleGrant,
|
||||||
revokeRoleGrant,
|
revokeRoleGrant,
|
||||||
replaceRoleGrants,
|
replaceRoleGrants,
|
||||||
} from './queries/capabilities';
|
} from './capabilities/queries';
|
||||||
export type { RoleGrant } from './queries/capabilities';
|
export type { RoleGrant } from './capabilities/queries';
|
||||||
export type { CapabilityLevelValue } from './schema/capabilities';
|
export type { CapabilityLevelValue } from './capabilities/schema';
|
||||||
export {
|
export {
|
||||||
getVaultTokens,
|
getVaultTokens,
|
||||||
setVaultTokens,
|
setVaultTokens,
|
||||||
@@ -234,8 +234,8 @@ export {
|
|||||||
getVaultUnlockKey,
|
getVaultUnlockKey,
|
||||||
setVaultUnlockKey,
|
setVaultUnlockKey,
|
||||||
clearVaultUnlockKey,
|
clearVaultUnlockKey,
|
||||||
} from './queries/vault';
|
} from './vault/queries';
|
||||||
export type { VaultTokenSet } from './queries/vault';
|
export type { VaultTokenSet } from './vault/queries';
|
||||||
export {
|
export {
|
||||||
listWallets,
|
listWallets,
|
||||||
getWallet,
|
getWallet,
|
||||||
@@ -254,7 +254,7 @@ export {
|
|||||||
getWalletChainCache,
|
getWalletChainCache,
|
||||||
saveWalletChainCache,
|
saveWalletChainCache,
|
||||||
recordWalletChainError,
|
recordWalletChainError,
|
||||||
} from './queries/wallet';
|
} from './wallet/queries';
|
||||||
export type {
|
export type {
|
||||||
WalletKind,
|
WalletKind,
|
||||||
WalletSummary,
|
WalletSummary,
|
||||||
@@ -262,13 +262,13 @@ export type {
|
|||||||
WalletLabel,
|
WalletLabel,
|
||||||
CreateWalletParams,
|
CreateWalletParams,
|
||||||
WalletChainCache,
|
WalletChainCache,
|
||||||
} from './queries/wallet';
|
} from './wallet/queries';
|
||||||
export type { WalletChainSnapshot } from './schema/wallet';
|
export type { WalletChainSnapshot } from './wallet/schema';
|
||||||
|
|
||||||
// Exported as a value, not just a type: the API and the UI need to enumerate the roles, and the
|
// Exported as a value, not just a type: the API and the UI need to enumerate the roles, and the
|
||||||
// column definition is the only place that list should exist.
|
// column definition is the only place that list should exist.
|
||||||
export { USER_ROLES, OWNER_USER_ID } from './schema/auth';
|
export { USER_ROLES, OWNER_USER_ID } from './auth/schema';
|
||||||
export type { UserRole } from './schema/auth';
|
export type { UserRole } from './auth/schema';
|
||||||
|
|
||||||
export { db } from './db';
|
export { db } from './db';
|
||||||
export * as schema from './schema';
|
export * as schema from './schema';
|
||||||
@@ -279,7 +279,7 @@ export {
|
|||||||
deletePushDevice,
|
deletePushDevice,
|
||||||
recordPushFailure,
|
recordPushFailure,
|
||||||
markPushDeviceSeen,
|
markPushDeviceSeen,
|
||||||
} from './queries/notify';
|
} from './notify/queries';
|
||||||
export type { PushDeviceSelect, PushDeviceInsert } from './types';
|
export type { PushDeviceSelect, PushDeviceInsert } from './types';
|
||||||
|
|
||||||
// App store — what the owner has installed, and whether it should be running.
|
// App store — what the owner has installed, and whether it should be running.
|
||||||
@@ -294,4 +294,4 @@ export {
|
|||||||
setEnabled,
|
setEnabled,
|
||||||
removeInstall,
|
removeInstall,
|
||||||
type SidecarInstall,
|
type SidecarInstall,
|
||||||
} from './queries/sidecar-installs';
|
} from './app-store/queries';
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
import { eq, and, sql } from 'drizzle-orm';
|
import { eq, and, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { serverIntegrations, userIntegrations } from '../schema';
|
import { serverIntegrations } from '../server/schema';
|
||||||
import { users } from '../schema/auth';
|
import { userIntegrations } from '../user-data/schema';
|
||||||
|
import { users } from '../auth/schema';
|
||||||
import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types';
|
import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types';
|
||||||
|
|
||||||
// ── Server Integrations ──
|
// ── Server Integrations ──
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { invoiceshelfAccounts } from '../schema/invoiceshelf';
|
import { invoiceshelfAccounts } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption
|
// InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// The InvoiceShelf accounts behind /invoices, for the officer-invoiceshelf sidecar.
|
// The InvoiceShelf accounts behind /invoices, for the officer-invoiceshelf sidecar.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { jellyfinServers } from '../schema/jellyfin';
|
import { jellyfinServers } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Jellyfin server registry for the officer-jellyfin sidecar. Callers deal in PLAINTEXT — encryption to and
|
// Jellyfin server registry for the officer-jellyfin sidecar. Callers deal in PLAINTEXT — encryption to and
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// The Jellyfin servers behind /jellyfin, for the officer-jellyfin sidecar.
|
// The Jellyfin servers behind /jellyfin, for the officer-jellyfin sidecar.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, desc, asc, sql } from 'drizzle-orm';
|
import { eq, and, desc, asc, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from '../schema/music';
|
import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from './schema';
|
||||||
|
|
||||||
export type FavoriteKind = 'track' | 'album' | 'artist';
|
export type FavoriteKind = 'track' | 'album' | 'artist';
|
||||||
export type GroupedFavorites = { tracks: string[]; albums: string[]; artists: string[] };
|
export type GroupedFavorites = { tracks: string[]; albums: string[]; artists: string[] };
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets:
|
// Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets:
|
||||||
// track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id)
|
// track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id)
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, sql } from 'drizzle-orm';
|
import { eq, and, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { pushDevices } from '../schema/notify';
|
import { pushDevices } from './schema';
|
||||||
import type { PushDeviceSelect, PushDeviceInsert } from '../types';
|
import type { PushDeviceSelect, PushDeviceInsert } from '../types';
|
||||||
|
|
||||||
// The push device registry. Only the officer-notify sidecar uses these.
|
// The push device registry. Only the officer-notify sidecar uses these.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Devices that can receive a push, for the officer-notify sidecar.
|
// Devices that can receive a push, for the officer-notify sidecar.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
export const taskLogs = pgTable('task_logs', {
|
export const taskLogs = pgTable('task_logs', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { photosConfig } from '../schema/photos';
|
import { photosConfig } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from
|
// Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// The Immich accounts behind /photos, for the officer-photos sidecar.
|
// The Immich accounts behind /photos, for the officer-photos sidecar.
|
||||||
//
|
//
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { eq, and, inArray, asc, desc } from 'drizzle-orm';
|
|||||||
|
|
||||||
const TERMINAL_STATUSES = ['completed', 'failed', 'stopped', 'interrupted'] as const;
|
const TERMINAL_STATUSES = ['completed', 'failed', 'stopped', 'interrupted'] as const;
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { pipelineJobs } from '../schema/pipeline-jobs';
|
import { pipelineJobs } from './schema';
|
||||||
import type { PipelineJobInsert } from '../types';
|
import type { PipelineJobInsert } from '../types';
|
||||||
|
|
||||||
export async function createPipelineJob(data: PipelineJobInsert) {
|
export async function createPipelineJob(data: PipelineJobInsert) {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
import { pgTable, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
export const pipelineJobs = pgTable(
|
export const pipelineJobs = pgTable(
|
||||||
'pipeline_jobs',
|
'pipeline_jobs',
|
||||||
+23
-23
@@ -21,35 +21,35 @@
|
|||||||
|
|
||||||
// ── Core ─────────────────────────────────────────────────────────────────────────────────────────
|
// ── Core ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export * from './auth'; // users, passkeys, passkey_challenges, token_blacklist
|
export * from './auth/schema'; // users, passkeys, passkey_challenges, token_blacklist
|
||||||
export * from './capabilities'; // role_capabilities — what each ROLE may reach
|
export * from './capabilities/schema'; // role_capabilities — what each ROLE may reach
|
||||||
export * from './api-keys'; // api_keys
|
export * from './api-keys/schema'; // api_keys
|
||||||
export * from './user-data'; // user_settings, user_state, user_integrations, dock_configs
|
export * from './user-data/schema'; // user_settings, user_state, user_integrations, dock_configs
|
||||||
export * from './dashboards'; // dashboards, screens, dashboard_defaults
|
export * from './dashboards/schema'; // dashboards, screens, dashboard_defaults
|
||||||
export * from './server'; // server_config (SMTP lives here), server_integrations
|
export * from './server/schema'; // server_config (SMTP lives here), server_integrations
|
||||||
export * from './operations'; // task_logs, queue_jobs, terminal_containers
|
export * from './operations/schema'; // task_logs, queue_jobs, terminal_containers
|
||||||
export * from './pipeline-jobs'; // pipeline_jobs
|
export * from './pipeline-jobs/schema'; // pipeline_jobs
|
||||||
export * from './chat-events'; // chat_session_events
|
export * from './chat-events/schema'; // chat_session_events
|
||||||
export * from './agent-panels'; // agent_panels
|
export * from './agent-panels/schema'; // agent_panels
|
||||||
|
|
||||||
// Core because the tailnet is the perimeter — a security model resting on it cannot treat administering
|
// Core because the tailnet is the perimeter — a security model resting on it cannot treat administering
|
||||||
// it as an optional extra. The secret store bootstraps a `headscale` key on this basis.
|
// it as an optional extra. The secret store bootstraps a `headscale` key on this basis.
|
||||||
export * from './headscale'; // headscale_servers
|
export * from './headscale/schema'; // headscale_servers
|
||||||
|
|
||||||
// The app store itself, and the credentials it stores for what it installs. `app-store/effects.ts`
|
// The app store itself, and the credentials it stores for what it installs. `app-store/effects.ts`
|
||||||
// reads service_connections, so this is core however few plugins are installed.
|
// reads service_connections, so this is core however few plugins are installed.
|
||||||
export * from './app-store'; // sidecar_installs
|
export * from './app-store/schema'; // sidecar_installs
|
||||||
export * from './service-connections'; // service_connections
|
export * from './service-connections/schema'; // service_connections
|
||||||
|
|
||||||
// ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
|
// ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
|
||||||
|
|
||||||
// export * from './email'; // email_accounts officer-email
|
// export * from './email/schema'; // email_accounts officer-email
|
||||||
// export * from './music'; // music_favorites, _playlists, _playlist_items, _now_playing
|
// export * from './music/schema'; // music_favorites, _playlists, _playlist_items, _now_playing
|
||||||
// export * from './notify'; // push_devices officer-notify
|
// export * from './notify/schema'; // push_devices officer-notify
|
||||||
// export * from './dav'; // dav_app_passwords officer-caldav
|
// export * from './dav/schema'; // dav_app_passwords officer-caldav
|
||||||
// export * from './photos'; // photos_config officer-photos
|
// export * from './photos/schema'; // photos_config officer-photos
|
||||||
// export * from './jellyfin'; // jellyfin_servers officer-jellyfin
|
// export * from './jellyfin/schema'; // jellyfin_servers officer-jellyfin
|
||||||
// export * from './invoiceshelf'; // invoiceshelf_accounts officer-invoiceshelf
|
// export * from './invoiceshelf/schema'; // invoiceshelf_accounts officer-invoiceshelf
|
||||||
// export * from './soulseek'; // soulseek_favorites, _browse_snapshots, _browse_dirs
|
// export * from './soulseek/schema'; // soulseek_favorites, _browse_snapshots, _browse_dirs
|
||||||
// export * from './vault'; // vault_tokens, vault_unlock_keys officer-vault
|
// export * from './vault/schema'; // vault_tokens, vault_unlock_keys officer-vault
|
||||||
// export * from './wallet'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache
|
// export * from './wallet/schema'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { serverConfig } from '../schema';
|
import { serverConfig } from './schema';
|
||||||
|
|
||||||
const SETTINGS_KEY = 'server-settings';
|
const SETTINGS_KEY = 'server-settings';
|
||||||
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { serviceConnections } from '../schema';
|
import { serviceConnections } from './schema';
|
||||||
import { getOwnerUser } from './auth';
|
import { getOwnerUser } from '../auth/queries';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT —
|
// Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT —
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Where a self-hosted service lives, and what it takes to talk to it — for services the owner has exactly
|
// Where a self-hosted service lives, and what it takes to talk to it — for services the owner has exactly
|
||||||
// ONE of. Transmission and slskd today.
|
// ONE of. Transmission and slskd today.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, asc, isNull, inArray, sql } from 'drizzle-orm';
|
import { eq, and, asc, isNull, inArray, sql } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from '../schema/soulseek';
|
import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from './schema';
|
||||||
|
|
||||||
/** A user's favourited Soulseek peers, alphabetical (the order the UI lists them in). */
|
/** A user's favourited Soulseek peers, alphabetical (the order the UI lists them in). */
|
||||||
export async function getSoulseekFavorites(userId: number): Promise<string[]> {
|
export async function getSoulseekFavorites(userId: number): Promise<string[]> {
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
|||||||
foreignKey,
|
foreignKey,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Soulseek state that Officer owns because slskd has none. slskd exposes no favourites/buddy-list API
|
// Soulseek state that Officer owns because slskd has none. slskd exposes no favourites/buddy-list API
|
||||||
// (verified against 0.26.0's UsersController: only endpoint/browse/directory/info/status), so the peers
|
// (verified against 0.26.0's UsersController: only endpoint/browse/directory/info/status), so the peers
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { dockConfigs, userSettings, userState } from '../schema';
|
import { dockConfigs, userSettings, userState } from './schema';
|
||||||
|
|
||||||
// ── User Settings ──
|
// ── User Settings ──
|
||||||
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { pgTable, serial, integer, text, timestamp, jsonb, check, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, timestamp, jsonb, check, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { serverIntegrations } from './server';
|
import { serverIntegrations } from '../server/schema';
|
||||||
|
|
||||||
export const userSettings = pgTable('user_settings', {
|
export const userSettings = pgTable('user_settings', {
|
||||||
userId: integer('user_id')
|
userId: integer('user_id')
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { vaultTokens, vaultUnlockKeys } from '../schema/vault';
|
import { vaultTokens, vaultUnlockKeys } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Vault store access. Callers deal in PLAINTEXT — encryption to/from at-rest ciphertext happens here, so
|
// Vault store access. Callers deal in PLAINTEXT — encryption to/from at-rest ciphertext happens here, so
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core';
|
import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Officer Vault server-side state (see VAULT_AUTH_SPEC.md). The device never holds a Vaultwarden token;
|
// Officer Vault server-side state (see VAULT_AUTH_SPEC.md). The device never holds a Vaultwarden token;
|
||||||
// the platform brokers it, stores it here tied to the owner account, and injects it on proxied /api/vault
|
// the platform brokers it, stores it here tied to the owner account, and injects it on proxied /api/vault
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import type { WalletChainSnapshot } from '../schema/wallet';
|
import type { WalletChainSnapshot } from './schema';
|
||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from '../schema/wallet';
|
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from './schema';
|
||||||
import { encryptSecret, decryptSecret } from '../crypto';
|
import { encryptSecret, decryptSecret } from '../crypto';
|
||||||
|
|
||||||
// Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet'
|
// Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet'
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { users } from './auth';
|
import { users } from '../auth/schema';
|
||||||
|
|
||||||
// Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a
|
// Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a
|
||||||
// self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning /
|
// self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning /
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
// auth+forward proxy and holds NO InvoiceShelf credentials.
|
// auth+forward proxy and holds NO InvoiceShelf credentials.
|
||||||
//
|
//
|
||||||
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `invoiceshelf_accounts` (see
|
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `invoiceshelf_accounts` (see
|
||||||
// databases/officer_db/src/queries/invoiceshelf.ts). It is deliberately no longer read from the environment:
|
// databases/officer_db/src/invoiceshelf/queries.ts). It is deliberately no longer read from the environment:
|
||||||
// Bun auto-loads `.env` into every process started in the platform directory, so an `INVOICESHELF_TOKEN`
|
// Bun auto-loads `.env` into every process started in the platform directory, so an `INVOICESHELF_TOKEN`
|
||||||
// there was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no
|
// there was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no
|
||||||
// code to use it and the largest attack surface in the system. Nothing in this file reads process.env.
|
// code to use it and the largest attack surface in the system. Nothing in this file reads process.env.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// thin auth+forward proxy and holds NO Jellyfin credentials.
|
// thin auth+forward proxy and holds NO Jellyfin credentials.
|
||||||
//
|
//
|
||||||
// The server is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `jellyfin_servers` (see
|
// The server is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `jellyfin_servers` (see
|
||||||
// databases/officer_db/src/queries/jellyfin.ts). Nothing in this file reads process.env — Bun auto-loads
|
// databases/officer_db/src/jellyfin/queries.ts). Nothing in this file reads process.env — Bun auto-loads
|
||||||
// `.env` into every process started in the platform directory, so a token there would also be sitting in
|
// `.env` into every process started in the platform directory, so a token there would also be sitting in
|
||||||
// `officer`'s own environment: a credential held by the one process that has no code to use it.
|
// `officer`'s own environment: a credential held by the one process that has no code to use it.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// auth+forward proxy and holds NO Immich credentials.
|
// auth+forward proxy and holds NO Immich credentials.
|
||||||
//
|
//
|
||||||
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `photos_config` (see
|
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `photos_config` (see
|
||||||
// databases/officer_db/src/queries/photos.ts). It is deliberately no longer read from the environment:
|
// databases/officer_db/src/photos/queries.ts). It is deliberately no longer read from the environment:
|
||||||
// Bun auto-loads `.env` into every process started in the platform directory, so an `IMMICH_API_KEY` there
|
// Bun auto-loads `.env` into every process started in the platform directory, so an `IMMICH_API_KEY` there
|
||||||
// was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no code
|
// was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no code
|
||||||
// to use it and the largest attack surface in the system. Nothing in this file reads process.env.
|
// to use it and the largest attack surface in the system. Nothing in this file reads process.env.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
|
|||||||
// auth+forward proxy and holds NO slskd credentials.
|
// auth+forward proxy and holds NO slskd credentials.
|
||||||
//
|
//
|
||||||
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see
|
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see
|
||||||
// databases/officer_db/src/queries/service-connections.ts). It is deliberately no longer read from the
|
// databases/officer_db/src/service-connections/queries.ts). It is deliberately no longer read from the
|
||||||
// environment: Bun auto-loads `.env` into every process started in the platform directory, so an
|
// environment: Bun auto-loads `.env` into every process started in the platform directory, so an
|
||||||
// `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole
|
// `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole
|
||||||
// Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env.
|
// Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
|
|||||||
// a thin auth+forward proxy and holds NO Transmission credentials.
|
// a thin auth+forward proxy and holds NO Transmission credentials.
|
||||||
//
|
//
|
||||||
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see
|
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see
|
||||||
// databases/officer_db/src/queries/service-connections.ts), no longer read from the environment: Bun
|
// databases/officer_db/src/service-connections/queries.ts), no longer read from the environment: Bun
|
||||||
// auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also
|
// auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also
|
||||||
// sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the
|
// sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the
|
||||||
// server. Nothing in this file reads process.env.
|
// server. Nothing in this file reads process.env.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getKey } from 'officerdb/secret-store';
|
|||||||
|
|
||||||
// The ONLY reader of WALLET_* env in the tree. Everything else — node URLs, macaroons, runes, LNDHub
|
// The ONLY reader of WALLET_* env in the tree. Everything else — node URLs, macaroons, runes, LNDHub
|
||||||
// credentials, NWC URIs — is per-wallet configuration the owner enters at runtime and lives encrypted in
|
// credentials, NWC URIs — is per-wallet configuration the owner enters at runtime and lives encrypted in
|
||||||
// Postgres (databases/officer_db/src/schema/wallet.ts), not here. Env holds only what is genuinely
|
// Postgres (databases/officer_db/src/wallet/schema.ts), not here. Env holds only what is genuinely
|
||||||
// deployment-wide: which chain we're on.
|
// deployment-wide: which chain we're on.
|
||||||
//
|
//
|
||||||
// WHERE CHAIN DATA COMES FROM IS NOT ENV. It used to be WALLET_ESPLORA_URL, which meant the one setting
|
// WHERE CHAIN DATA COMES FROM IS NOT ENV. It used to be WALLET_ESPLORA_URL, which meant the one setting
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Shared types/constants for the /wallet workspace panels.
|
// Shared types/constants for the /wallet workspace panels.
|
||||||
//
|
//
|
||||||
// The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in
|
// The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in
|
||||||
// src/databases/officer_db/src/queries/wallet.ts. They are restated here rather than imported because the
|
// src/databases/officer_db/src/wallet/queries.ts. They are restated here rather than imported because the
|
||||||
// officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser
|
// officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser
|
||||||
// bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never
|
// bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never
|
||||||
// reach the client. Keep this file in step with those two by hand; the field names are identical on
|
// reach the client. Keep this file in step with those two by hand; the field names are identical on
|
||||||
|
|||||||
Reference in New Issue
Block a user