From d12da18579550eb4f1d8ccbf0db010c5282c8a9f Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 04:58:58 +0900
Subject: [PATCH 1/8] Add root domain
---
packages/drfed/src/index.ts | 4 ++--
packages/drfed/src/parser.ts | 9 ++++++++-
packages/graphql/src/builder.ts | 7 ++++++-
packages/graphql/src/index.ts | 6 ++++++
scripts/dev.mts | 1 +
5 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts
index 077b548..1195156 100644
--- a/packages/drfed/src/index.ts
+++ b/packages/drfed/src/index.ts
@@ -44,8 +44,8 @@ async function runServer(options: ServerOptions) {
if (options.seed) {
await seedData(options.drizzle.db);
}
- const { mailer } = options;
- const yogaServer = createYogaServer(options.drizzle.db, { mailer });
+ const { mailer, root } = options;
+ const yogaServer = createYogaServer(options.drizzle.db, { root, mailer });
const server = serve({
fetch: yogaServer.fetch.bind(yogaServer),
hostname: options.address.host,
diff --git a/packages/drfed/src/parser.ts b/packages/drfed/src/parser.ts
index 0828e36..b7beff0 100644
--- a/packages/drfed/src/parser.ts
+++ b/packages/drfed/src/parser.ts
@@ -22,7 +22,7 @@ import { message, optionNames } from "@optique/core/message";
import { map, optional, withDefault } from "@optique/core/modifiers";
import type { InferValue } from "@optique/core/parser";
import { flag, option } from "@optique/core/primitives";
-import { socketAddress, url } from "@optique/core/valueparser";
+import { domain, socketAddress, url } from "@optique/core/valueparser";
import { loggingOptions } from "@optique/logtape";
import { path } from "@optique/run/valueparser";
import { LogTapeTransport } from "@upyo/logtape";
@@ -105,6 +105,12 @@ const seedParser = option("--dev-seed", {
hidden: true,
});
+const rootParser = optional(
+ option("--root-domain", "-r", domain({ lowercase: true }), {
+ description: message`The root domain of host.`,
+ }),
+);
+
const serverParser = object("DrFed server", {
address: withDefault(
option("--listen", "-l", socketAddress({ requirePort: true }), {
@@ -126,6 +132,7 @@ const serverParser = object("DrFed server", {
),
}),
),
+ root: rootParser,
mailer: smtpParser,
seed: seedParser,
});
diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts
index 2a08a30..88e20e2 100644
--- a/packages/graphql/src/builder.ts
+++ b/packages/graphql/src/builder.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
import { type Database, normalizeEmail, relations } from "@drfed/models";
-import type { Account, Session } from "@drfed/models/schema";
+import { type Account, type Session } from "@drfed/models/schema";
import { Template } from "@fedify/uri-template";
import SchemaBuilder, { type ObjectRef } from "@pothos/core";
import DrizzlePlugin from "@pothos/plugin-drizzle";
@@ -56,6 +56,11 @@ export interface ServerContext {
* Origin list.
*/
readonly origins: ReadonlySet;
+
+ /**
+ * Root domain.
+ */
+ readonly root: string;
}
/**
diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts
index 9e991b4..9ddbfa0 100644
--- a/packages/graphql/src/index.ts
+++ b/packages/graphql/src/index.ts
@@ -47,6 +47,11 @@ export interface YogaServerOptions {
* Origin list.
*/
origins?: ReadonlySet;
+
+ /**
+ * Root domain.
+ */
+ root?: string | undefined;
}
/**
@@ -104,6 +109,7 @@ const fillOptions = (opt: YogaServerOptions): Required => ({
"http://localhost:5173",
"http://0.0.0.0:5173",
]),
+ root: opt?.root ?? "drfed.org",
});
const getAccessToken = (headers: Headers) =>
diff --git a/scripts/dev.mts b/scripts/dev.mts
index 4d94252..88711ea 100644
--- a/scripts/dev.mts
+++ b/scripts/dev.mts
@@ -306,6 +306,7 @@ try {
"../../.pgdata",
"--listen=0.0.0.0:8888",
"--log-format=color",
+ "--root-domain=drfed.org",
];
const logLevel = process.env.usage_log_level;
From 4e386b16d5f967a46641af67eac68cf146301921 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 04:59:13 +0900
Subject: [PATCH 2/8] Ignore `node/no-top-level-await`
---
.oxlintrc.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 9126f90..b955275 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -68,6 +68,7 @@
"import/prefer-default-export": "off",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
+ "node/no-top-level-await": "off",
"promise/avoid-new": "warn"
},
"overrides": [
From 1b473077995ebd36a5e5d17f1c8646f3cb32671d Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 05:21:03 +0900
Subject: [PATCH 3/8] Separate local/remote schema
---
packages/models/src/relations.ts | 20 ++++++++++
packages/models/src/schema.ts | 67 +++++++++++++++++++++++---------
2 files changed, 69 insertions(+), 18 deletions(-)
diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts
index c1848df..d8f2e51 100644
--- a/packages/models/src/relations.ts
+++ b/packages/models/src/relations.ts
@@ -72,6 +72,26 @@ export const relations = defineRelations(schema, (r) => ({
accepted: { isNotNull: true },
},
}),
+ localInstances: r.one.localInstances({
+ from: r.instances.id,
+ to: r.localInstances.id,
+ }),
+ remoteInstances: r.one.remoteInstances({
+ from: r.instances.id,
+ to: r.remoteInstances.id,
+ }),
+ },
+ localInstance: {
+ instances: r.one.instances({
+ from: r.localInstances.id,
+ to: r.instances.id,
+ }),
+ },
+ remoteInstance: {
+ instances: r.one.instances({
+ from: r.remoteInstances.id,
+ to: r.instances.id,
+ }),
},
sessions: {
account: r.one.accounts({
diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts
index 6693785..518cef0 100644
--- a/packages/models/src/schema.ts
+++ b/packages/models/src/schema.ts
@@ -13,19 +13,26 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+
+// oxlint-disable max-lines
+
import { sql } from "drizzle-orm";
import {
boolean,
check,
index,
integer,
+ pgEnum,
pgTable,
primaryKey,
+ text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
+const currentTimestamp = sql`CURRENT_TIMESTAMP`;
+
/**
* The database table to represent accounts.
*/
@@ -39,7 +46,7 @@ export const accounts = pgTable(
admin: boolean().notNull().default(false),
created: timestamp({ withTimezone: true })
.notNull()
- .default(sql`CURRENT_TIMESTAMP`),
+ .default(currentTimestamp),
},
(table) => [
check(
@@ -54,30 +61,54 @@ export const accounts = pgTable(
export type Account = typeof accounts.$inferSelect;
export type NewAccount = typeof accounts.$inferInsert;
+export const locationEnum = pgEnum("location", ["Local", "Remote"]);
+export type Location = (typeof locationEnum.enumValues)[number];
+
/**
* The database table to represent instances.
*/
-export const instances = pgTable(
- "instances",
+export const instances = pgTable("instances", {
+ id: uuid().primaryKey(),
+ location: locationEnum().notNull(),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(currentTimestamp),
+});
+
+export type Instance = typeof instances.$inferSelect;
+export type NewInstance = typeof instances.$inferInsert;
+
+export const localInstances = pgTable(
+ "local_instances",
{
- id: uuid().primaryKey(),
- slug: varchar({ length: 100 }).notNull().unique(),
+ id: uuid()
+ .primaryKey()
+ .references(() => instances.id, { onDelete: "cascade" }),
+ slug: varchar({ length: 63 }).notNull().unique(),
expires: timestamp({ withTimezone: true }).notNull(),
- created: timestamp({ withTimezone: true })
- .notNull()
- .default(sql`CURRENT_TIMESTAMP`),
+ maxActors: integer().notNull().default(10),
},
(table) => [
- check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,100}$'`),
- check(
- "instances_expires_check",
- sql`${table.expires} < (${table.created} + INTERVAL '1 year')`,
- ),
+ check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,63}$'`),
+ check("instances_max_actors_check", sql`${table.maxActors} > 0`),
],
);
-export type Instance = typeof instances.$inferSelect;
-export type NewInstance = typeof instances.$inferInsert;
+export type LocalInstance = typeof localInstances.$inferSelect;
+export type NewLocalInstance = typeof localInstances.$inferInsert;
+
+export const remoteInstances = pgTable("remote_instances", {
+ id: uuid()
+ .primaryKey()
+ .references(() => instances.id, { onDelete: "cascade" }),
+ host: varchar({ length: 100 }).notNull().unique(),
+ nodeInfoUrl: text(),
+ software: text(),
+ softwareVersion: text(),
+});
+
+export type RemoteInstance = typeof remoteInstances.$inferSelect;
+export type NewRemoteInstance = typeof remoteInstances.$inferInsert;
/**
* The association table between instances and its member accounts.
@@ -97,7 +128,7 @@ export const instanceMembers = pgTable(
accepted: timestamp({ withTimezone: true }),
created: timestamp({ withTimezone: true })
.notNull()
- .default(sql`CURRENT_TIMESTAMP`),
+ .default(currentTimestamp),
},
(table) => [
primaryKey({ columns: [table.instanceId, table.accountId] }),
@@ -128,7 +159,7 @@ export const loginTokens = pgTable("login_tokens", {
codeHash: varchar({ length: 64 }).notNull(),
created: timestamp({ withTimezone: true })
.notNull()
- .default(sql`CURRENT_TIMESTAMP`),
+ .default(currentTimestamp),
expires: timestamp({ withTimezone: true })
.notNull()
.default(sql`CURRENT_TIMESTAMP + INTERVAL '15 minutes'`),
@@ -150,7 +181,7 @@ export const sessions = pgTable("sessions", {
tokenHash: varchar({ length: 64 }).notNull().unique(),
created: timestamp({ withTimezone: true })
.notNull()
- .default(sql`CURRENT_TIMESTAMP`),
+ .default(currentTimestamp),
expires: timestamp({ withTimezone: true })
.notNull()
.default(sql`CURRENT_TIMESTAMP + INTERVAL '1 month'`),
From 19e1d14f64292d266495cde2a0f9ee9c5f1b166b Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 05:22:30 +0900
Subject: [PATCH 4/8] Add host, location and remove slug, expires when get
Instance
Add , and remove , when get
---
packages/graphql/src/builder.ts | 6 +++-
packages/graphql/src/instance.test.ts | 13 ++++++--
packages/graphql/src/instance.ts | 48 +++++++++++++++++++++------
3 files changed, 53 insertions(+), 14 deletions(-)
diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts
index 88e20e2..b52152f 100644
--- a/packages/graphql/src/builder.ts
+++ b/packages/graphql/src/builder.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
import { type Database, normalizeEmail, relations } from "@drfed/models";
-import { type Account, type Session } from "@drfed/models/schema";
+import type { Account, Session, Location } from "@drfed/models/schema";
import { Template } from "@fedify/uri-template";
import SchemaBuilder, { type ObjectRef } from "@pothos/core";
import DrizzlePlugin from "@pothos/plugin-drizzle";
@@ -90,6 +90,10 @@ export interface SchemaTypes {
Input: string;
Output: string;
};
+ Location: {
+ Input: Location;
+ Output: Location;
+ };
UUID: {
Input: string;
Output: string;
diff --git a/packages/graphql/src/instance.test.ts b/packages/graphql/src/instance.test.ts
index cc32334..aa3c6ba 100644
--- a/packages/graphql/src/instance.test.ts
+++ b/packages/graphql/src/instance.test.ts
@@ -151,7 +151,12 @@ describe("Mutation.createInstance", () => {
const instances = await db.select().from(schema.instances);
assert.equal(instances.length, 1);
- assert.equal(instances[0]?.slug, "my-instance");
+ const instance = instances[0]!;
+ assert.equal(instance.location, "Local");
+ const local = await db.query.localInstances.findFirst({
+ where: instance,
+ });
+ assert.equal(local?.slug, "my-instance");
const members = await db.select().from(schema.instanceMembers);
assert.equal(members.length, 1);
@@ -294,8 +299,12 @@ async function seedInstanceMembers(db: Database): Promise {
]);
await db.insert(schema.instances).values({
id: instanceId,
- slug: "test-instance",
+ location: "Local",
created,
+ });
+ await db.insert(schema.localInstances).values({
+ id: instanceId,
+ slug: "test-instance",
expires,
});
await db.insert(schema.instanceMembers).values([
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
index 32cdd5c..d5caddb 100644
--- a/packages/graphql/src/instance.ts
+++ b/packages/graphql/src/instance.ts
@@ -40,10 +40,28 @@ const InstanceRef = builder.drizzleNode("instances", {
type: "UUID",
description: "The UUID of the `Instance`.",
}),
- slug: t.exposeString("slug"),
- expires: t.expose("expires", {
- type: "DateTime",
- description: "The expiration date/time of the `Instance`.",
+ location: t.expose("location", {
+ type: "Location",
+ description: 'The location of the `Instance`: "Local" | "Remote"',
+ }),
+ host: t.string({
+ async resolve({ id, location }, _, { db, root }) {
+ if (location === "Local") {
+ const ins = await db.query.localInstances.findFirst({
+ columns: { slug: true },
+ where: { id },
+ });
+ if (ins == null) throwUncontested(id);
+ return `${ins.slug}.${root}`;
+ }
+ const ins = await db.query.remoteInstances.findFirst({
+ columns: { host: true },
+ where: { id },
+ });
+ if (ins == null) throwUncontested(id);
+ return ins.host;
+ },
+ description: "The host of the `Instance`.",
}),
created: t.expose("created", {
type: "DateTime",
@@ -52,6 +70,10 @@ const InstanceRef = builder.drizzleNode("instances", {
}),
});
+function throwUncontested(id: string): never {
+ throw new Error(`DB consistency is broken.: ${id}`);
+}
+
export const Instance: DrFedObjectRef = InstanceRef;
const instanceMembersConnection = drizzleConnectionHelpers(
@@ -218,15 +240,10 @@ builder.mutationFields((t) => ({
let tooManyInstances = false;
try {
return await ctx.db.transaction(async (tx) => {
+ const id = uuid();
const [instance] = await tx
.insert(schema.instances)
- .values({
- id: uuid(),
- slug,
- expires: new Date(
- Temporal.Now.instant().add({ hours: 8750 }).toString(),
- ),
- })
+ .values({ id, location: "Local" })
.returning();
if (instance == null) throw new Error("Failed to create instance.");
await tx.insert(schema.instanceMembers).values({
@@ -241,6 +258,13 @@ builder.mutationFields((t) => ({
tooManyInstances = true;
tx.rollback();
}
+ tx.insert(schema.localInstances).values({
+ id,
+ slug,
+ expires: new Date(
+ Temporal.Now.instant().add({ hours: YEAR_BY_HOURS }).toString(),
+ ),
+ });
return instance;
});
} catch (e) {
@@ -266,3 +290,5 @@ builder.mutationFields((t) => ({
},
}),
}));
+
+const YEAR_BY_HOURS = 8760;
From 0ef857bc86f21c661e88c5c6d4f51a1bece6d06c Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 06:02:06 +0900
Subject: [PATCH 5/8] Generate migration
---
.../migration.sql | 26 +
.../snapshot.json | 768 ++++++++++++++++++
2 files changed, 794 insertions(+)
create mode 100644 packages/models/drizzle/20260802205417_separate-instance-local-remote/migration.sql
create mode 100644 packages/models/drizzle/20260802205417_separate-instance-local-remote/snapshot.json
diff --git a/packages/models/drizzle/20260802205417_separate-instance-local-remote/migration.sql b/packages/models/drizzle/20260802205417_separate-instance-local-remote/migration.sql
new file mode 100644
index 0000000..7b30cef
--- /dev/null
+++ b/packages/models/drizzle/20260802205417_separate-instance-local-remote/migration.sql
@@ -0,0 +1,26 @@
+CREATE TYPE "location" AS ENUM('Local', 'Remote');--> statement-breakpoint
+CREATE TABLE "local_instances" (
+ "id" uuid PRIMARY KEY,
+ "slug" varchar(63) NOT NULL UNIQUE,
+ "expires" timestamp with time zone NOT NULL,
+ "maxActors" integer DEFAULT 10 NOT NULL,
+ CONSTRAINT "instances_slug_check" CHECK ("slug" ~ '^[a-z0-9-]{4,63}$'),
+ CONSTRAINT "instances_max_actors_check" CHECK ("maxActors" > 0)
+);
+--> statement-breakpoint
+CREATE TABLE "remote_instances" (
+ "id" uuid PRIMARY KEY,
+ "host" varchar(100) NOT NULL UNIQUE,
+ "nodeInfoUrl" text,
+ "software" text,
+ "softwareVersion" text
+);
+--> statement-breakpoint
+ALTER TABLE "instances" DROP CONSTRAINT "instances_slug_key";--> statement-breakpoint
+ALTER TABLE "instances" DROP CONSTRAINT "instances_slug_check";--> statement-breakpoint
+ALTER TABLE "instances" DROP CONSTRAINT "instances_expires_check";--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "location" "location" NOT NULL;--> statement-breakpoint
+ALTER TABLE "instances" DROP COLUMN "slug";--> statement-breakpoint
+ALTER TABLE "instances" DROP COLUMN "expires";--> statement-breakpoint
+ALTER TABLE "local_instances" ADD CONSTRAINT "local_instances_id_instances_id_fkey" FOREIGN KEY ("id") REFERENCES "instances"("id") ON DELETE CASCADE;--> statement-breakpoint
+ALTER TABLE "remote_instances" ADD CONSTRAINT "remote_instances_id_instances_id_fkey" FOREIGN KEY ("id") REFERENCES "instances"("id") ON DELETE CASCADE;
\ No newline at end of file
diff --git a/packages/models/drizzle/20260802205417_separate-instance-local-remote/snapshot.json b/packages/models/drizzle/20260802205417_separate-instance-local-remote/snapshot.json
new file mode 100644
index 0000000..fa02ce8
--- /dev/null
+++ b/packages/models/drizzle/20260802205417_separate-instance-local-remote/snapshot.json
@@ -0,0 +1,768 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "08f1dfcb-1110-4550-9777-36d017ecfb10",
+ "prevIds": ["d11b25cc-8a54-4c9f-95c5-1891ce2a133a"],
+ "ddl": [
+ {
+ "values": ["Local", "Remote"],
+ "name": "location",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "accounts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instance_members",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "local_instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "login_tokens",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "remote_instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "sessions",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "max_instances",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instanceId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accepted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "location",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "varchar(63)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "slug",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "maxActors",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "codeHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '15 minutes'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "consumed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "host",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "nodeInfoUrl",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "software",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "softwareVersion",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '1 month'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "accountId",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_accountId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "instanceId",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_instanceId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instanceId"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_instanceId_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "local_instances_id_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "login_tokens_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "remote_instances_id_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "sessions_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "columns": ["instanceId", "accountId"],
+ "nameExplicit": false,
+ "name": "instance_members_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "accounts_pkey",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "instances_pkey",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "local_instances_pkey",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "login_tokens_pkey",
+ "schema": "public",
+ "table": "login_tokens",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "remote_instances_pkey",
+ "schema": "public",
+ "table": "remote_instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sessions_pkey",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["email"],
+ "nullsNotDistinct": false,
+ "name": "accounts_email_key",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["slug"],
+ "nullsNotDistinct": false,
+ "name": "local_instances_slug_key",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["tokenHash"],
+ "nullsNotDistinct": false,
+ "name": "login_tokens_tokenHash_key",
+ "schema": "public",
+ "table": "login_tokens",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["host"],
+ "nullsNotDistinct": false,
+ "name": "remote_instances_host_key",
+ "schema": "public",
+ "table": "remote_instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["tokenHash"],
+ "nullsNotDistinct": false,
+ "name": "sessions_tokenHash_key",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "uniques"
+ },
+ {
+ "value": "\"email\" ~ '^[^@]+@[^@]+\\.[^@]+$'",
+ "name": "accounts_email_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"max_instances\" >= 0",
+ "name": "accounts_max_instances_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "trim(both from \"name\") <> ''",
+ "name": "accounts_name_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"slug\" ~ '^[a-z0-9-]{4,63}$'",
+ "name": "instances_slug_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "\"maxActors\" > 0",
+ "name": "instances_max_actors_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ }
+ ],
+ "renames": []
+}
From fab0c9fe6219b6737eb9d4fa4a64deb90561bd33 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 06:04:59 +0900
Subject: [PATCH 6/8] Generate test code by AI agents
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prompt:
DB 스키마에서 `instance` 를 Local/Remote 로 분리하고 API 를 수정하면서 기존 스키마/API 에 맞춰놨던 일부 테스트가 문제 되고 있습니다. [instance.test.ts](packages/graphql/src/instance.test.ts) 파일의 20419cea220545130243b6787ad1122a95ef1462 커밋 변경 사항을 참고해서 문제되는 파일들의 코드를 직접 수정하세요. 문제되는 파일들은 `mise check`, `mise test` 로 찾을 수 있습니다.
추가적으로 리모트 인스턴스를 위한 테스트도 추가하세요. 너무 복잡한 테스트 말고, 생성, host 유일성 체크 정도만 추가하세요.
Assisted-by: Codex:gpt-5-6-sol
---
packages/graphql/src/account.test.ts | 48 ++++++++----
packages/graphql/src/builder.ts | 6 +-
packages/graphql/src/instance.test.ts | 105 +++++++++++++++++++++++++-
packages/graphql/src/instance.ts | 12 ++-
4 files changed, 143 insertions(+), 28 deletions(-)
diff --git a/packages/graphql/src/account.test.ts b/packages/graphql/src/account.test.ts
index 060dc6a..c20ca5c 100644
--- a/packages/graphql/src/account.test.ts
+++ b/packages/graphql/src/account.test.ts
@@ -52,7 +52,8 @@ const accountInstancesQuery = `
admin
node {
uuid
- slug
+ location
+ host
}
}
}
@@ -82,7 +83,8 @@ const accountInstancesResponse = {
admin: true,
node: {
uuid: acceptedInstanceId,
- slug: "test-instance",
+ location: "Local",
+ host: "test-instance.drfed.org",
},
},
],
@@ -148,20 +150,7 @@ async function seedAccounts(db: Database): Promise {
async function seedMembershipGraph(db: Database): Promise {
await seedAccounts(db);
- await db.insert(schema.instances).values([
- {
- id: acceptedInstanceId,
- slug: "test-instance",
- created,
- expires,
- },
- {
- id: pendingInstanceId,
- slug: "pending-instance",
- created,
- expires,
- },
- ]);
+ await seedLocalInstances(db);
await db.insert(schema.instanceMembers).values([
{
accountId,
@@ -193,3 +182,30 @@ async function seedMembershipGraph(db: Database): Promise {
},
]);
}
+
+async function seedLocalInstances(db: Database): Promise {
+ await db.insert(schema.instances).values([
+ {
+ id: acceptedInstanceId,
+ location: "Local",
+ created,
+ },
+ {
+ id: pendingInstanceId,
+ location: "Local",
+ created,
+ },
+ ]);
+ await db.insert(schema.localInstances).values([
+ {
+ id: acceptedInstanceId,
+ slug: "test-instance",
+ expires,
+ },
+ {
+ id: pendingInstanceId,
+ slug: "pending-instance",
+ expires,
+ },
+ ]);
+}
diff --git a/packages/graphql/src/builder.ts b/packages/graphql/src/builder.ts
index b52152f..d6925cc 100644
--- a/packages/graphql/src/builder.ts
+++ b/packages/graphql/src/builder.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
import { type Database, normalizeEmail, relations } from "@drfed/models";
-import type { Account, Session, Location } from "@drfed/models/schema";
+import type { Account, Session } from "@drfed/models/schema";
import { Template } from "@fedify/uri-template";
import SchemaBuilder, { type ObjectRef } from "@pothos/core";
import DrizzlePlugin from "@pothos/plugin-drizzle";
@@ -90,10 +90,6 @@ export interface SchemaTypes {
Input: string;
Output: string;
};
- Location: {
- Input: Location;
- Output: Location;
- };
UUID: {
Input: string;
Output: string;
diff --git a/packages/graphql/src/instance.test.ts b/packages/graphql/src/instance.test.ts
index aa3c6ba..b24ca04 100644
--- a/packages/graphql/src/instance.test.ts
+++ b/packages/graphql/src/instance.test.ts
@@ -19,6 +19,7 @@ import assert from "node:assert/strict";
import { type Database, schema } from "@drfed/models";
import { describe, it } from "@logtape/testing-node/autoload";
+import { DrizzleQueryError } from "drizzle-orm";
import { withTestHarness } from "./harness.test.ts";
@@ -31,9 +32,26 @@ const accountId = "00000000-0000-4000-8000-000000000001";
const memberId = "00000000-0000-4000-8000-000000000002";
const pendingMemberId = "00000000-0000-4000-8000-000000000003";
const instanceId = "00000000-0000-4000-8000-000000000101";
+const duplicateRemoteInstanceId = "00000000-0000-4000-8000-000000000102";
const sessionId = "00000000-0000-4000-8000-000000000201";
const accessToken = "test-access-token";
+const remoteInstanceQuery = `
+ query RemoteInstance($uuid: UUID!) {
+ accountByUuid(uuid: $uuid) {
+ instances {
+ edges {
+ node {
+ uuid
+ location
+ host
+ }
+ }
+ }
+ }
+ }
+`;
+
const instanceMembersQuery = `
query InstanceMembers($uuid: UUID!) {
accountByUuid(uuid: $uuid) {
@@ -122,7 +140,8 @@ const createInstanceMutation = `
__typename
... on Instance {
uuid
- slug
+ location
+ host
}
... on CreateInstanceError {
type
@@ -146,7 +165,8 @@ describe("Mutation.createInstance", () => {
const body = await response.json();
assert.equal(body.errors, undefined);
assert.equal(body.data.createInstance.__typename, "Instance");
- assert.equal(body.data.createInstance.slug, "my-instance");
+ assert.equal(body.data.createInstance.location, "Local");
+ assert.equal(body.data.createInstance.host, "my-instance.drfed.org");
assert.equal(typeof body.data.createInstance.uuid, "string");
const instances = await db.select().from(schema.instances);
@@ -154,7 +174,7 @@ describe("Mutation.createInstance", () => {
const instance = instances[0]!;
assert.equal(instance.location, "Local");
const local = await db.query.localInstances.findFirst({
- where: instance,
+ where: { id: instance.id },
});
assert.equal(local?.slug, "my-instance");
@@ -231,6 +251,61 @@ describe("Mutation.createInstance", () => {
});
});
+describe("Remote instance", () => {
+ it("returns a created remote instance", async () => {
+ await withTestHarness(async ({ db, post }) => {
+ await seedRemoteInstance(db);
+
+ const response = await post({
+ query: remoteInstanceQuery,
+ variables: { uuid: accountId },
+ });
+
+ assert.equal(response.status, ok);
+ assert.deepEqual(await response.json(), {
+ data: {
+ accountByUuid: {
+ instances: {
+ edges: [
+ {
+ node: {
+ uuid: instanceId,
+ location: "Remote",
+ host: "remote.example.com",
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+ });
+ });
+
+ it("requires a unique host", async () => {
+ await withTestHarness(async ({ db }) => {
+ await seedRemoteInstance(db);
+ await db.insert(schema.instances).values({
+ id: duplicateRemoteInstanceId,
+ location: "Remote",
+ created,
+ });
+
+ await assert.rejects(
+ db.insert(schema.remoteInstances).values({
+ id: duplicateRemoteInstanceId,
+ host: "remote.example.com",
+ }),
+ (error: unknown) =>
+ error instanceof DrizzleQueryError &&
+ error.cause != null &&
+ "constraint" in error.cause &&
+ error.cause.constraint === "remote_instances_host_key",
+ );
+ });
+ });
+});
+
/**
* Seeds an account and an authenticated session, then returns the request
* options carrying the session's bearer token.
@@ -331,3 +406,27 @@ async function seedInstanceMembers(db: Database): Promise {
},
]);
}
+
+async function seedRemoteInstance(db: Database): Promise {
+ await db.insert(schema.accounts).values({
+ id: accountId,
+ email: "owner@example.com",
+ name: "Owner",
+ created,
+ });
+ await db.insert(schema.instances).values({
+ id: instanceId,
+ location: "Remote",
+ created,
+ });
+ await db.insert(schema.remoteInstances).values({
+ id: instanceId,
+ host: "remote.example.com",
+ });
+ await db.insert(schema.instanceMembers).values({
+ accountId,
+ instanceId,
+ accepted,
+ created,
+ });
+}
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
index d5caddb..73649b7 100644
--- a/packages/graphql/src/instance.ts
+++ b/packages/graphql/src/instance.ts
@@ -16,7 +16,7 @@
// oxlint-disable max-lines-per-function
import { schema } from "@drfed/models";
-import { instanceMembers } from "@drfed/models/schema";
+import { instanceMembers, locationEnum } from "@drfed/models/schema";
import { drizzleConnectionHelpers } from "@pothos/plugin-drizzle";
import { DrizzleQueryError } from "drizzle-orm";
import { and, eq, isNotNull } from "drizzle-orm/sql/expressions";
@@ -26,6 +26,10 @@ import { v7 as uuid } from "uuid";
import { Account } from "./account.ts";
import builder, { type DrFedObjectRef } from "./builder.ts";
+const Location = builder.enumType("Location", {
+ values: locationEnum.enumValues,
+});
+
const InstanceRef = builder.drizzleNode("instances", {
name: "Instance",
description: "Represents an `Instance` in the DrFed platform.",
@@ -41,7 +45,7 @@ const InstanceRef = builder.drizzleNode("instances", {
description: "The UUID of the `Instance`.",
}),
location: t.expose("location", {
- type: "Location",
+ type: Location,
description: 'The location of the `Instance`: "Local" | "Remote"',
}),
host: t.string({
@@ -258,7 +262,7 @@ builder.mutationFields((t) => ({
tooManyInstances = true;
tx.rollback();
}
- tx.insert(schema.localInstances).values({
+ await tx.insert(schema.localInstances).values({
id,
slug,
expires: new Date(
@@ -278,7 +282,7 @@ builder.mutationFields((t) => ({
e instanceof DrizzleQueryError &&
e.cause != null &&
"constraint" in e.cause &&
- e.cause.constraint === "instances_slug_key"
+ e.cause.constraint === "local_instances_slug_key"
) {
return {
message: `The slug ${JSON.stringify(slug)} is already taken.`,
From 59c87cee56bcdb51a3b48ad2f0347cb6b44fd3b7 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 4 Aug 2026 14:42:11 +0900
Subject: [PATCH 7/8] Add constraints to prevent simultaneous Local/Remote
registration
---
.../migration.sql | 7 +
.../snapshot.json | 843 ++++++++++++++++++
packages/models/src/schema.ts | 72 +-
3 files changed, 904 insertions(+), 18 deletions(-)
create mode 100644 packages/models/drizzle/20260804061119_instances-local-remote-constraint/migration.sql
create mode 100644 packages/models/drizzle/20260804061119_instances-local-remote-constraint/snapshot.json
diff --git a/packages/models/drizzle/20260804061119_instances-local-remote-constraint/migration.sql b/packages/models/drizzle/20260804061119_instances-local-remote-constraint/migration.sql
new file mode 100644
index 0000000..563fe43
--- /dev/null
+++ b/packages/models/drizzle/20260804061119_instances-local-remote-constraint/migration.sql
@@ -0,0 +1,7 @@
+ALTER TABLE "local_instances" ADD COLUMN "location" "location" DEFAULT 'Local'::"location" NOT NULL;--> statement-breakpoint
+ALTER TABLE "remote_instances" ADD COLUMN "location" "location" DEFAULT 'Remote'::"location" NOT NULL;--> statement-breakpoint
+ALTER TABLE "instances" ADD CONSTRAINT "instances_id_location_key" UNIQUE("id","location");--> statement-breakpoint
+ALTER TABLE "local_instances" ADD CONSTRAINT "local_instance_fk" FOREIGN KEY ("id","location") REFERENCES "instances"("id","location") ON DELETE CASCADE;--> statement-breakpoint
+ALTER TABLE "remote_instances" ADD CONSTRAINT "remote_instance_fk" FOREIGN KEY ("id","location") REFERENCES "instances"("id","location") ON DELETE CASCADE;--> statement-breakpoint
+ALTER TABLE "local_instances" ADD CONSTRAINT "local_check" CHECK ("location" = 'Local');--> statement-breakpoint
+ALTER TABLE "remote_instances" ADD CONSTRAINT "remote_check" CHECK ("location" = 'Remote');
\ No newline at end of file
diff --git a/packages/models/drizzle/20260804061119_instances-local-remote-constraint/snapshot.json b/packages/models/drizzle/20260804061119_instances-local-remote-constraint/snapshot.json
new file mode 100644
index 0000000..c980d7c
--- /dev/null
+++ b/packages/models/drizzle/20260804061119_instances-local-remote-constraint/snapshot.json
@@ -0,0 +1,843 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "c40a9b41-e120-4f01-865b-68583fbb921c",
+ "prevIds": ["08f1dfcb-1110-4550-9777-36d017ecfb10"],
+ "ddl": [
+ {
+ "values": ["Local", "Remote"],
+ "name": "location",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "accounts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instance_members",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "local_instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "login_tokens",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "remote_instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "sessions",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "max_instances",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instanceId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accepted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "location",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "varchar(63)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "slug",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "maxActors",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "location",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'Local'",
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "codeHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '15 minutes'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "consumed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "host",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "nodeInfoUrl",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "software",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "softwareVersion",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "location",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'Remote'",
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenHash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '1 month'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "accountId",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_accountId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "instanceId",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_instanceId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instanceId"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_instanceId_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "local_instances_id_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["id", "location"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id", "location"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "local_instance_fk",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "login_tokens_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "login_tokens"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "remote_instances_id_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["id", "location"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id", "location"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "remote_instance_fk",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "remote_instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["accountId"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "sessions_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "columns": ["instanceId", "accountId"],
+ "nameExplicit": false,
+ "name": "instance_members_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "accounts_pkey",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "instances_pkey",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "local_instances_pkey",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "login_tokens_pkey",
+ "schema": "public",
+ "table": "login_tokens",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "remote_instances_pkey",
+ "schema": "public",
+ "table": "remote_instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sessions_pkey",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["id", "location"],
+ "nullsNotDistinct": false,
+ "name": "instances_id_location_key",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["email"],
+ "nullsNotDistinct": false,
+ "name": "accounts_email_key",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["slug"],
+ "nullsNotDistinct": false,
+ "name": "local_instances_slug_key",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["tokenHash"],
+ "nullsNotDistinct": false,
+ "name": "login_tokens_tokenHash_key",
+ "schema": "public",
+ "table": "login_tokens",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["host"],
+ "nullsNotDistinct": false,
+ "name": "remote_instances_host_key",
+ "schema": "public",
+ "table": "remote_instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["tokenHash"],
+ "nullsNotDistinct": false,
+ "name": "sessions_tokenHash_key",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "uniques"
+ },
+ {
+ "value": "\"email\" ~ '^[^@]+@[^@]+\\.[^@]+$'",
+ "name": "accounts_email_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"max_instances\" >= 0",
+ "name": "accounts_max_instances_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "trim(both from \"name\") <> ''",
+ "name": "accounts_name_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"slug\" ~ '^[a-z0-9-]{4,63}$'",
+ "name": "instances_slug_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "\"maxActors\" > 0",
+ "name": "instances_max_actors_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "\"location\" = 'Local'",
+ "name": "local_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "\"location\" = 'Remote'",
+ "name": "remote_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "remote_instances"
+ }
+ ],
+ "renames": []
+}
diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts
index 518cef0..ba44d01 100644
--- a/packages/models/src/schema.ts
+++ b/packages/models/src/schema.ts
@@ -14,12 +14,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-// oxlint-disable max-lines
-
import { sql } from "drizzle-orm";
import {
boolean,
check,
+ foreignKey,
index,
integer,
pgEnum,
@@ -27,6 +26,7 @@ import {
primaryKey,
text,
timestamp,
+ unique,
uuid,
varchar,
} from "drizzle-orm/pg-core";
@@ -67,13 +67,20 @@ export type Location = (typeof locationEnum.enumValues)[number];
/**
* The database table to represent instances.
*/
-export const instances = pgTable("instances", {
- id: uuid().primaryKey(),
- location: locationEnum().notNull(),
- created: timestamp({ withTimezone: true })
- .notNull()
- .default(currentTimestamp),
-});
+export const instances = pgTable(
+ "instances",
+ {
+ id: uuid().primaryKey(),
+ location: locationEnum().notNull(),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(currentTimestamp),
+ },
+ (t) => [
+ // This unique constraint prevent simultaneous Local/Remote registration.
+ unique("instances_id_location_key").on(t.id, t.location),
+ ],
+);
export type Instance = typeof instances.$inferSelect;
export type NewInstance = typeof instances.$inferInsert;
@@ -87,25 +94,54 @@ export const localInstances = pgTable(
slug: varchar({ length: 63 }).notNull().unique(),
expires: timestamp({ withTimezone: true }).notNull(),
maxActors: integer().notNull().default(10),
+ // This `location` column is not an actually used value;
+ // it exists to prevent simultaneous Local/Remote registration,
+ // so it must be fixed as `Local`.
+ location: locationEnum().default("Local").notNull(),
},
(table) => [
check("instances_slug_check", sql`${table.slug} ~ '^[a-z0-9-]{4,63}$'`),
check("instances_max_actors_check", sql`${table.maxActors} > 0`),
+ // Following `location` check and FK prevent simultaneous Local/Remote
+ // registration.
+ check("local_check", sql`${table.location} = 'Local'`),
+ foreignKey({
+ name: "local_instance_fk",
+ columns: [table.id, table.location],
+ foreignColumns: [instances.id, instances.location],
+ }).onDelete("cascade"),
],
);
export type LocalInstance = typeof localInstances.$inferSelect;
export type NewLocalInstance = typeof localInstances.$inferInsert;
-export const remoteInstances = pgTable("remote_instances", {
- id: uuid()
- .primaryKey()
- .references(() => instances.id, { onDelete: "cascade" }),
- host: varchar({ length: 100 }).notNull().unique(),
- nodeInfoUrl: text(),
- software: text(),
- softwareVersion: text(),
-});
+export const remoteInstances = pgTable(
+ "remote_instances",
+ {
+ id: uuid()
+ .primaryKey()
+ .references(() => instances.id, { onDelete: "cascade" }),
+ host: varchar({ length: 100 }).notNull().unique(),
+ nodeInfoUrl: text(),
+ software: text(),
+ softwareVersion: text(),
+ // This `location` column is not an actually used value;
+ // it exists to prevent simultaneous Local/Remote registration,
+ // so it must be fixed as `Remote`.
+ location: locationEnum().default("Remote").notNull(),
+ },
+ (table) => [
+ // Following `location` check and FK prevent simultaneous Local/Remote
+ // registration.
+ check("remote_check", sql`${table.location} = 'Remote'`),
+ foreignKey({
+ name: "remote_instance_fk",
+ columns: [table.id, table.location],
+ foreignColumns: [instances.id, instances.location],
+ }).onDelete("cascade"),
+ ],
+);
export type RemoteInstance = typeof remoteInstances.$inferSelect;
export type NewRemoteInstance = typeof remoteInstances.$inferInsert;
From 39e033eb9f764957a013c06ece1ff206b9ac57b2 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 4 Aug 2026 14:47:06 +0900
Subject: [PATCH 8/8] Add oxlint rules
https://github.com/fedify-dev/drfed/pull/44#pullrequestreview-4850930385
https://github.com/fedify-dev/drfed/pull/44#pullrequestreview-4850931835
---
.oxlintrc.json | 1 +
packages/drfed/src/index.ts | 1 -
packages/graphql/src/account.ts | 1 -
packages/graphql/src/auth.test.ts | 2 +-
packages/graphql/src/instance.test.ts | 3 +--
packages/graphql/src/instance.ts | 2 --
packages/models/src/relations.ts | 2 +-
scripts/dev.mts | 2 +-
8 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/.oxlintrc.json b/.oxlintrc.json
index b955275..f7b6e6e 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -30,6 +30,7 @@
"eslint/eqeqeq": ["off", "smart"],
"eslint/func-style": ["off"],
"eslint/max-lines-per-function": "off",
+ "eslint/max-lines": "off",
"eslint/id-length": ["warn", { "exceptionPatterns": ["^_", "^[Tertv]$"] }],
"eslint/init-declarations": "off",
"eslint/max-params": ["warn", { "max": 4 }],
diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts
index 1195156..9b633cd 100644
--- a/packages/drfed/src/index.ts
+++ b/packages/drfed/src/index.ts
@@ -76,7 +76,6 @@ async function runSchemaGenerator(
await writeFile(options.outputFile, schemaCode, { encoding: "utf-8" });
}
-// oxlint-disable-next-line max-lines-per-function
export async function main(): Promise {
const options: Options = run(program, {
help: "option",
diff --git a/packages/graphql/src/account.ts b/packages/graphql/src/account.ts
index 0451807..f07b118 100644
--- a/packages/graphql/src/account.ts
+++ b/packages/graphql/src/account.ts
@@ -79,7 +79,6 @@ const accountInstancesConnection = drizzleConnectionHelpers(
},
);
-// oxlint-disable-next-line max-lines-per-function
builder.drizzleObjectField(AccountRef, "instances", (t) =>
t.connection(
{
diff --git a/packages/graphql/src/auth.test.ts b/packages/graphql/src/auth.test.ts
index 88673ec..fc5dd4f 100644
--- a/packages/graphql/src/auth.test.ts
+++ b/packages/graphql/src/auth.test.ts
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-// oxlint-disable max-lines-per-function max-statements no-magic-numbers
+// oxlint-disable max-statements no-magic-numbers
import { deepEqual, equal, ok } from "node:assert/strict";
diff --git a/packages/graphql/src/instance.test.ts b/packages/graphql/src/instance.test.ts
index b24ca04..00ab79e 100644
--- a/packages/graphql/src/instance.test.ts
+++ b/packages/graphql/src/instance.test.ts
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-// oxlint-disable max-lines max-lines-per-function no-underscore-dangle
+// oxlint-disable no-underscore-dangle
import assert from "node:assert/strict";
import { type Database, schema } from "@drfed/models";
@@ -347,7 +347,6 @@ async function hashSecret(raw: string): Promise {
).toHex();
}
-// oxlint-disable-next-line max-lines-per-function
async function seedInstanceMembers(db: Database): Promise {
await db.insert(schema.accounts).values([
{
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
index 73649b7..c95f5f5 100644
--- a/packages/graphql/src/instance.ts
+++ b/packages/graphql/src/instance.ts
@@ -14,7 +14,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-// oxlint-disable max-lines-per-function
import { schema } from "@drfed/models";
import { instanceMembers, locationEnum } from "@drfed/models/schema";
import { drizzleConnectionHelpers } from "@pothos/plugin-drizzle";
@@ -103,7 +102,6 @@ const instanceMembersConnection = drizzleConnectionHelpers(
},
);
-// oxlint-disable-next-line max-lines-per-function
builder.drizzleObjectField(InstanceRef, "members", (t) =>
t.connection(
{
diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts
index d8f2e51..104376e 100644
--- a/packages/models/src/relations.ts
+++ b/packages/models/src/relations.ts
@@ -13,11 +13,11 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema.ts";
-// oxlint-disable-next-line eslint/max-lines-per-function
export const relations = defineRelations(schema, (r) => ({
accounts: {
instances: r.many.instances({
diff --git a/scripts/dev.mts b/scripts/dev.mts
index 88711ea..595642d 100644
--- a/scripts/dev.mts
+++ b/scripts/dev.mts
@@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-// oxlint-disable no-console no-magic-numbers eslin/max-lines node/no-top-level-await
+// oxlint-disable no-console no-magic-numbers node/no-top-level-await
import { type ChildProcess, spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { readFile, readdir, rm } from "node:fs/promises";