From dd308ef4a78ec09e0fb2e9d023bb96396505f0b9 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 04:58:58 +0900
Subject: [PATCH 01/12] 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 2105fcc..afd8ddd 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;
}
/**
@@ -98,6 +103,7 @@ const fillOptions = (opt: YogaServerOptions): Required => ({
emailFrom: opt?.emailFrom ?? "noreply@drfed.org",
// FIXME: Properly parametrize the following allowlist:
origins: opt?.origins ?? new Set(["https://drfed.org"]),
+ 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 c4713d85fa683b8e5f91d799d8be08cca7c64dc9 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 04:59:13 +0900
Subject: [PATCH 02/12] Ignore `node/no-top-level-await`
---
.oxlintrc.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/.oxlintrc.json b/.oxlintrc.json
index 1f94a12..98a88b2 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -69,6 +69,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 e99b348fe1155a589673f312f5063dd8bf11a7f0 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 05:21:03 +0900
Subject: [PATCH 03/12] 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 3f7f67596eafa0a8a50ea077b1b19cc2ce833143 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 05:22:30 +0900
Subject: [PATCH 04/12] 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 562faf085ecca48936f1c81e52f659e843556f74 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 06:02:06 +0900
Subject: [PATCH 05/12] 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 3e68ed079ff7c8e99573e45916e0b294a0969f3e Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Mon, 3 Aug 2026 06:04:59 +0900
Subject: [PATCH 06/12] 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 55cbc2eb7a9d19aaba952874183d08461913f7be Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 4 Aug 2026 14:42:11 +0900
Subject: [PATCH 07/12] 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 3a937f691698f0486f988a32242496e1f6115d1d Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 4 Aug 2026 14:47:06 +0900
Subject: [PATCH 08/12] 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 98a88b2..10405cf 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";
From 500aca2f4265ce3220569787c8896d21fe84bfc1 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 18 Aug 2026 15:46:51 +0900
Subject: [PATCH 09/12] Add .env.example
---
packages/web/.env.example | 1 +
1 file changed, 1 insertion(+)
create mode 100644 packages/web/.env.example
diff --git a/packages/web/.env.example b/packages/web/.env.example
new file mode 100644
index 0000000..3a69b97
--- /dev/null
+++ b/packages/web/.env.example
@@ -0,0 +1 @@
+VITE_BACKEND_URL=http://127.0.0.1:8888
From 09be000bdd6a7c9e032651c582d99d46b4402046 Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 18 Aug 2026 17:28:29 +0900
Subject: [PATCH 10/12] Merge remoteInstances into instances
---
mise.toml | 1 +
packages/graphql/src/instance.ts | 93 ++-
.../migration.sql | 15 +
.../snapshot.json | 722 ++++++++++++++++++
packages/models/src/relations.ts | 14 +-
packages/models/src/schema.ts | 79 +-
.../src/routes/workspace/create/instance.tsx | 10 +-
7 files changed, 810 insertions(+), 124 deletions(-)
create mode 100644 packages/models/drizzle/20260818080259_dark_night_thrasher/migration.sql
create mode 100644 packages/models/drizzle/20260818080259_dark_night_thrasher/snapshot.json
diff --git a/mise.toml b/mise.toml
index 5154641..a8e44c3 100644
--- a/mise.toml
+++ b/mise.toml
@@ -127,6 +127,7 @@ flag "--name " help="Migration file name"
flag "--custom" help="Prepare empty migration file for custom SQL"
"""
dir = "packages/models"
+raw = true
run = """
#!/usr/bin/env nu
let name = (if "usage_name" in $env { ["--name" $env.usage_name] } else { [] })
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
index c95f5f5..ff62eea 100644
--- a/packages/graphql/src/instance.ts
+++ b/packages/graphql/src/instance.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
import { schema } from "@drfed/models";
-import { instanceMembers, locationEnum } from "@drfed/models/schema";
+import { instanceMembers } from "@drfed/models/schema";
import { drizzleConnectionHelpers } from "@pothos/plugin-drizzle";
import { DrizzleQueryError } from "drizzle-orm";
import { and, eq, isNotNull } from "drizzle-orm/sql/expressions";
@@ -25,10 +25,6 @@ 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,44 +37,51 @@ const InstanceRef = builder.drizzleNode("instances", {
fields: (t) => ({
uuid: t.expose("id", {
type: "UUID",
- description: "The UUID 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`.",
}),
+ host: t.exposeString("host"),
created: t.expose("created", {
type: "DateTime",
description: "The creation date/time of the `Instance`.",
}),
+ nodeInfoUrl: t.exposeString("nodeInfoUrl", {
+ nullable: true,
+ }),
+ software: t.exposeString("software", {
+ nullable: true,
+ }),
+ softwareVersion: t.exposeString("softwareVersion", {
+ nullable: true,
+ }),
}),
});
-function throwUncontested(id: string): never {
- throw new Error(`DB consistency is broken.: ${id}`);
-}
-
export const Instance: DrFedObjectRef = InstanceRef;
+const LocalInstanceRef = builder.drizzleNode("localInstances", {
+ name: "LocalInstance",
+ description: "Represents an `Instance` in the DrFed platform.",
+ id: {
+ column(instance) {
+ return instance.id;
+ },
+ description: "The unique identifier of the `Instance`.",
+ },
+ fields: (t) => ({
+ uuid: t.expose("id", {
+ type: "UUID",
+ description: "The UUID of the `Instance`.",
+ }),
+ slug: t.exposeString("slug"),
+ expires: t.expose("expires", {
+ type: "DateTime",
+ description: "The expire date of the instance.",
+ }),
+ maxActors: t.exposeInt("maxActors"),
+ }),
+});
+
+export const LocalInstance: DrFedObjectRef = LocalInstanceRef;
+
const instanceMembersConnection = drizzleConnectionHelpers(
builder,
"instanceMembers",
@@ -242,10 +245,23 @@ builder.mutationFields((t) => ({
let tooManyInstances = false;
try {
return await ctx.db.transaction(async (tx) => {
- const id = uuid();
+ const [local] = await tx
+ .insert(schema.localInstances)
+ .values({
+ id: uuid(),
+ slug,
+ expires: new Date(
+ Temporal.Now.instant().add({ hours: YEAR_BY_HOURS }).toString(),
+ ),
+ })
+ .returning();
+ if (local == null) {
+ throw new Error("Failed to create local instance.");
+ }
+ const host = `${slug}.${ctx.root}`;
const [instance] = await tx
.insert(schema.instances)
- .values({ id, location: "Local" })
+ .values({ id: uuid(), host })
.returning();
if (instance == null) throw new Error("Failed to create instance.");
await tx.insert(schema.instanceMembers).values({
@@ -260,13 +276,6 @@ builder.mutationFields((t) => ({
tooManyInstances = true;
tx.rollback();
}
- await tx.insert(schema.localInstances).values({
- id,
- slug,
- expires: new Date(
- Temporal.Now.instant().add({ hours: YEAR_BY_HOURS }).toString(),
- ),
- });
return instance;
});
} catch (e) {
diff --git a/packages/models/drizzle/20260818080259_dark_night_thrasher/migration.sql b/packages/models/drizzle/20260818080259_dark_night_thrasher/migration.sql
new file mode 100644
index 0000000..8cda542
--- /dev/null
+++ b/packages/models/drizzle/20260818080259_dark_night_thrasher/migration.sql
@@ -0,0 +1,15 @@
+ALTER TABLE "local_instances" DROP CONSTRAINT "local_instances_id_instances_id_fkey";--> statement-breakpoint
+ALTER TABLE "local_instances" DROP CONSTRAINT "local_instance_fk";--> statement-breakpoint
+DROP TABLE "remote_instances";--> statement-breakpoint
+ALTER TABLE "instances" DROP CONSTRAINT "instances_id_location_key";--> statement-breakpoint
+ALTER TABLE "local_instances" DROP CONSTRAINT "local_check";--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "localId" uuid;--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "host" varchar(100) NOT NULL;--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "nodeInfoUrl" text;--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "software" text;--> statement-breakpoint
+ALTER TABLE "instances" ADD COLUMN "softwareVersion" text;--> statement-breakpoint
+ALTER TABLE "instances" DROP COLUMN "location";--> statement-breakpoint
+ALTER TABLE "local_instances" DROP COLUMN "location";--> statement-breakpoint
+ALTER TABLE "instances" ADD CONSTRAINT "instances_host_key" UNIQUE("host");--> statement-breakpoint
+ALTER TABLE "instances" ADD CONSTRAINT "instances_localId_local_instances_id_fkey" FOREIGN KEY ("localId") REFERENCES "local_instances"("id") ON DELETE CASCADE;--> statement-breakpoint
+DROP TYPE "location";
\ No newline at end of file
diff --git a/packages/models/drizzle/20260818080259_dark_night_thrasher/snapshot.json b/packages/models/drizzle/20260818080259_dark_night_thrasher/snapshot.json
new file mode 100644
index 0000000..7464399
--- /dev/null
+++ b/packages/models/drizzle/20260818080259_dark_night_thrasher/snapshot.json
@@ -0,0 +1,722 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "d8ac955f-1e74-4493-ade9-a25c40b01c20",
+ "prevIds": ["c40a9b41-e120-4f01-865b-68583fbb921c"],
+ "ddl": [
+ {
+ "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": "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": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "localId",
+ "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": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "host",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "nodeInfoUrl",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "software",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "softwareVersion",
+ "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": "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": ["localId"],
+ "schemaTo": "public",
+ "tableTo": "local_instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "instances_localId_local_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "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": ["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": "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": ["host"],
+ "nullsNotDistinct": false,
+ "name": "instances_host_key",
+ "schema": "public",
+ "table": "instances",
+ "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": ["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": []
+}
diff --git a/packages/models/src/relations.ts b/packages/models/src/relations.ts
index 104376e..299db4c 100644
--- a/packages/models/src/relations.ts
+++ b/packages/models/src/relations.ts
@@ -73,24 +73,14 @@ export const relations = defineRelations(schema, (r) => ({
},
}),
localInstances: r.one.localInstances({
- from: r.instances.id,
+ from: r.instances.localId,
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,
+ to: r.instances.localId,
}),
},
sessions: {
diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts
index ba44d01..d68bc1a 100644
--- a/packages/models/src/schema.ts
+++ b/packages/models/src/schema.ts
@@ -18,15 +18,12 @@ import { sql } from "drizzle-orm";
import {
boolean,
check,
- foreignKey,
index,
integer,
- pgEnum,
pgTable,
primaryKey,
text,
timestamp,
- unique,
uuid,
varchar,
} from "drizzle-orm/pg-core";
@@ -61,26 +58,22 @@ 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",
- {
- 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 const instances = pgTable("instances", {
+ id: uuid().primaryKey(),
+ localId: uuid().references(() => localInstances.id, {
+ onDelete: "cascade",
+ }),
+ created: timestamp({ withTimezone: true })
+ .notNull()
+ .default(currentTimestamp),
+ host: varchar({ length: 100 }).notNull().unique(),
+ nodeInfoUrl: text(),
+ software: text(),
+ softwareVersion: text(),
+});
export type Instance = typeof instances.$inferSelect;
export type NewInstance = typeof instances.$inferInsert;
@@ -88,64 +81,20 @@ export type NewInstance = typeof instances.$inferInsert;
export const localInstances = pgTable(
"local_instances",
{
- id: uuid()
- .primaryKey()
- .references(() => instances.id, { onDelete: "cascade" }),
+ id: uuid().primaryKey(),
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(),
- // 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;
-
/**
* The association table between instances and its member accounts.
* Note that it also contains the just invited members, which are not yet
diff --git a/packages/web/src/routes/workspace/create/instance.tsx b/packages/web/src/routes/workspace/create/instance.tsx
index a01d340..7e09a6b 100644
--- a/packages/web/src/routes/workspace/create/instance.tsx
+++ b/packages/web/src/routes/workspace/create/instance.tsx
@@ -31,7 +31,7 @@ const createInstanceMutation = graphql`
) {
createInstance(slug: $slug) {
... on Instance {
- slug
+ id
}
}
}
@@ -40,7 +40,7 @@ const createInstanceMutation = graphql`
type CreateInstanceResult =
| {
payload: {
- slug: string;
+ id: string;
};
status: "success";
}
@@ -82,15 +82,15 @@ const createInstanceAction = action(async (formData: FormData) => {
message: errorMessage,
status: "error",
});
- } else if (response.createInstance.slug === undefined) {
+ } else if (response.createInstance.id === undefined) {
resolve({
- message: "Empty Slug Returned",
+ message: "Empty ID Returned",
status: "error",
});
} else {
resolve({
payload: {
- slug: response.createInstance.slug,
+ id: response.createInstance.id,
},
status: "success",
});
From 296156e7741093e8bbf7f8154c7d82da510646cd Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 18 Aug 2026 17:28:40 +0900
Subject: [PATCH 11/12] Fix tests
---
packages/graphql/src/account.test.ts | 4 ++--
packages/graphql/src/instance.test.ts | 22 ++++++++--------------
2 files changed, 10 insertions(+), 16 deletions(-)
diff --git a/packages/graphql/src/account.test.ts b/packages/graphql/src/account.test.ts
index c20ca5c..ed506ee 100644
--- a/packages/graphql/src/account.test.ts
+++ b/packages/graphql/src/account.test.ts
@@ -187,12 +187,12 @@ async function seedLocalInstances(db: Database): Promise {
await db.insert(schema.instances).values([
{
id: acceptedInstanceId,
- location: "Local",
+ host: "temp1.drfed.org",
created,
},
{
id: pendingInstanceId,
- location: "Local",
+ host: "temp2.drfed.org",
created,
},
]);
diff --git a/packages/graphql/src/instance.test.ts b/packages/graphql/src/instance.test.ts
index 00ab79e..d5e08f2 100644
--- a/packages/graphql/src/instance.test.ts
+++ b/packages/graphql/src/instance.test.ts
@@ -172,7 +172,6 @@ describe("Mutation.createInstance", () => {
const instances = await db.select().from(schema.instances);
assert.equal(instances.length, 1);
const instance = instances[0]!;
- assert.equal(instance.location, "Local");
const local = await db.query.localInstances.findFirst({
where: { id: instance.id },
});
@@ -287,12 +286,11 @@ describe("Remote instance", () => {
await seedRemoteInstance(db);
await db.insert(schema.instances).values({
id: duplicateRemoteInstanceId,
- location: "Remote",
- created,
+ host: "example.com",
});
await assert.rejects(
- db.insert(schema.remoteInstances).values({
+ db.insert(schema.instances).values({
id: duplicateRemoteInstanceId,
host: "remote.example.com",
}),
@@ -371,16 +369,16 @@ async function seedInstanceMembers(db: Database): Promise {
created,
},
]);
- await db.insert(schema.instances).values({
- id: instanceId,
- location: "Local",
- created,
- });
await db.insert(schema.localInstances).values({
id: instanceId,
slug: "test-instance",
expires,
});
+ await db.insert(schema.instances).values({
+ id: instanceId,
+ created,
+ host: "test-instance.drfed.org",
+ });
await db.insert(schema.instanceMembers).values([
{
accountId,
@@ -415,12 +413,8 @@ async function seedRemoteInstance(db: Database): Promise {
});
await db.insert(schema.instances).values({
id: instanceId,
- location: "Remote",
created,
- });
- await db.insert(schema.remoteInstances).values({
- id: instanceId,
- host: "remote.example.com",
+ host: `example.com`,
});
await db.insert(schema.instanceMembers).values({
accountId,
From b28a3733e530b669e38e5d40de0d2640ad537c0d Mon Sep 17 00:00:00 2001
From: ChanHaeng Lee <2chanhaeng@gmail.com>
Date: Tue, 18 Aug 2026 17:35:46 +0900
Subject: [PATCH 12/12] Fix code with GPT 5.6 Sol
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prompt:
변경 사항이 있어 테스트가 깨지고 있습니다. mise test 로 테스트를 실행했을 때 발생하는 모든 문제를 정리한 뒤 테스트를 수정하세요.
Assisted-by: Codex:gpt-5-6-sol
---
packages/graphql/src/account.test.ts | 24 ++++++++++++------------
packages/graphql/src/instance.test.ts | 16 +++++-----------
packages/graphql/src/instance.ts | 2 +-
3 files changed, 18 insertions(+), 24 deletions(-)
diff --git a/packages/graphql/src/account.test.ts b/packages/graphql/src/account.test.ts
index ed506ee..bc00e39 100644
--- a/packages/graphql/src/account.test.ts
+++ b/packages/graphql/src/account.test.ts
@@ -52,7 +52,6 @@ const accountInstancesQuery = `
admin
node {
uuid
- location
host
}
}
@@ -83,7 +82,6 @@ const accountInstancesResponse = {
admin: true,
node: {
uuid: acceptedInstanceId,
- location: "Local",
host: "test-instance.drfed.org",
},
},
@@ -184,28 +182,30 @@ async function seedMembershipGraph(db: Database): Promise {
}
async function seedLocalInstances(db: Database): Promise {
- await db.insert(schema.instances).values([
+ await db.insert(schema.localInstances).values([
{
id: acceptedInstanceId,
- host: "temp1.drfed.org",
- created,
+ slug: "test-instance",
+ expires,
},
{
id: pendingInstanceId,
- host: "temp2.drfed.org",
- created,
+ slug: "pending-instance",
+ expires,
},
]);
- await db.insert(schema.localInstances).values([
+ await db.insert(schema.instances).values([
{
id: acceptedInstanceId,
- slug: "test-instance",
- expires,
+ localId: acceptedInstanceId,
+ host: "test-instance.drfed.org",
+ created,
},
{
id: pendingInstanceId,
- slug: "pending-instance",
- expires,
+ localId: pendingInstanceId,
+ host: "pending-instance.drfed.org",
+ created,
},
]);
}
diff --git a/packages/graphql/src/instance.test.ts b/packages/graphql/src/instance.test.ts
index d5e08f2..d707ce6 100644
--- a/packages/graphql/src/instance.test.ts
+++ b/packages/graphql/src/instance.test.ts
@@ -43,7 +43,6 @@ const remoteInstanceQuery = `
edges {
node {
uuid
- location
host
}
}
@@ -140,7 +139,6 @@ const createInstanceMutation = `
__typename
... on Instance {
uuid
- location
host
}
... on CreateInstanceError {
@@ -165,15 +163,15 @@ 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.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);
assert.equal(instances.length, 1);
const instance = instances[0]!;
+ assert.equal(typeof instance.localId, "string");
const local = await db.query.localInstances.findFirst({
- where: { id: instance.id },
+ where: { id: instance.localId! },
});
assert.equal(local?.slug, "my-instance");
@@ -269,7 +267,6 @@ describe("Remote instance", () => {
{
node: {
uuid: instanceId,
- location: "Remote",
host: "remote.example.com",
},
},
@@ -284,10 +281,6 @@ describe("Remote instance", () => {
it("requires a unique host", async () => {
await withTestHarness(async ({ db }) => {
await seedRemoteInstance(db);
- await db.insert(schema.instances).values({
- id: duplicateRemoteInstanceId,
- host: "example.com",
- });
await assert.rejects(
db.insert(schema.instances).values({
@@ -298,7 +291,7 @@ describe("Remote instance", () => {
error instanceof DrizzleQueryError &&
error.cause != null &&
"constraint" in error.cause &&
- error.cause.constraint === "remote_instances_host_key",
+ error.cause.constraint === "instances_host_key",
);
});
});
@@ -376,6 +369,7 @@ async function seedInstanceMembers(db: Database): Promise {
});
await db.insert(schema.instances).values({
id: instanceId,
+ localId: instanceId,
created,
host: "test-instance.drfed.org",
});
@@ -414,7 +408,7 @@ async function seedRemoteInstance(db: Database): Promise {
await db.insert(schema.instances).values({
id: instanceId,
created,
- host: `example.com`,
+ host: "remote.example.com",
});
await db.insert(schema.instanceMembers).values({
accountId,
diff --git a/packages/graphql/src/instance.ts b/packages/graphql/src/instance.ts
index ff62eea..f778f83 100644
--- a/packages/graphql/src/instance.ts
+++ b/packages/graphql/src/instance.ts
@@ -261,7 +261,7 @@ builder.mutationFields((t) => ({
const host = `${slug}.${ctx.root}`;
const [instance] = await tx
.insert(schema.instances)
- .values({ id: uuid(), host })
+ .values({ id: uuid(), localId: local.id, host })
.returning();
if (instance == null) throw new Error("Failed to create instance.");
await tx.insert(schema.instanceMembers).values({