Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: pubOp for creating and updating pubs more easily #965

Merged
merged 27 commits into from
Feb 12, 2025
Merged

RFC: pubOp for creating and updating pubs more easily #965

merged 27 commits into from
Feb 12, 2025

Conversation

tefkah
Copy link
Member

@tefkah tefkah commented Feb 11, 2025

Issue(s) Resolved

Adds a new PubOp system for creating, updating and managing relationships between Pubs using a fluent-like API/

High-level Explanation of PR

Note

This PR is an RFC! Meaning I'm totally happy not merging it!
I'm more interested in your general thoughts on this approach, please nitpick it to hell!

This PR introduces a new PubOp system that provides a fluent, builder-style API for managing Pub operations. The system handles complex scenarios like creating multiple related pubs, managing nested relationships, and handling pub value updates in a single transaction.

API

The API follows a builder pattern, allowing for chaining operations:

const pub = await PubOp.create({
  communityId: community.id,
  pubTypeId: pubType.id,
  lastModifiedBy: createLastModifiedBy({ userId: user.id }),
})
  .set('title', 'My Pub')
  .relate('someRelation', 'relation value', relatedPubId)
  .execute();

All builders

Currently there's 3 kinds of operations you can do:

  • create
  • update
  • upsert

For create, there's also an explicit createWithId.
For both update and upsert, there's an updateByValue and an upsertByValue. These allow you to, instead of selecting a pub by id, select a pub by a specific value, eg a google drive id.

Basic capabilities

Currently, the PubOp allows you to do a bunch of things, here's a small table that shows what you can do with each of the operations:

Capability Create Update Upsert
Set values
Connect/create related pubs
Set stage/move pub
Explicitly disconnect relations - -
Explicitly disconnect values - -
Override existing values -
Replace existing relations -
Set values
const pub = await PubOp.createWithId(pubId, {
  /*...*/
})
  .set('community-slug:title', 'My Pub')
  .execute();
Set many values in one go
const pub = await PubOp.upsert(pubId, {
  /*...*/
})
  .set({
    'community-slug:title': 'My Pub',
    'community-slug:description': 'My Pub description',
  })
  .execute();
Relate pubs
const pub = await PubOp.create({
  /*...*/
})
  .relate('community-slug:someRelation', 'relation value 1', relatedPubId1)
  .relate('community-slug:someRelation', 'relation value 2', relatedPubId2)
  .execute();
Relate multiple pubs in one go
const pub = await PubOp.create({
  /*...*/
})
  .relate('community-slug:someRelation', [
    {
      value: 'relation value 1',
      target: relatedPubId1,
    },
    {
      value: 'relation value 2',
      target: relatedPubId2,
    },
  ])
  .execute();
Nested relate

You can nest pub operations, allowing you to create, relate, and/or update multiple pubs in a single operation:

const pub = await PubOp.upsert(pubId, {
  /*...*/
})
  .relate(
    'someRelation',
    'value',
    PubOp.upsert(relatedPubId, {
      /*...*/
    })
      .set('title', 'Related Pub')
      .relate(
        'anotherRelation',
        'value2',
        PubOp.upsert(relatedPubId2, {
          /*...*/
        }).set('title', 'Related Pub 2')
      )
  )
  .execute();

If you don't want to keep repeating { communityId: ..., lastModifiedBy: ... }, you also define a function instead
This just returns a new PubOp with communityId etc already set.

You'll still need to provide the pubTypeId for upsert and create operations.

const pub = await PubOp.create({
  communityId,
  pubTypeId,
  lastModifiedBy: createLastModifiedBy({ userId }),
})
  .set('community-slug:title', 'Community Title')
  .relate('community-slug:someRelation', 'relation value', (pubOp) =>
    pubOp
      .create({
        pubTypeId,
      })
      .set('pub-type-slug:title', 'Nested Pub')
  )
  .execute();

Test Plan

  1. Look at the tests in

    import { beforeAll, describe, expect, it } from "vitest";
    import type { PubsId } from "db/public";
    import { CoreSchemaType, MemberRole } from "db/public";
    import type { CommunitySeedOutput } from "~/prisma/seed/createSeed";
    import { mockServerCode } from "~/lib/__tests__/utils";
    import { createLastModifiedBy } from "../lastModifiedBy";
    import { PubOp } from "./pub-op";
    const { createSeed } = await import("~/prisma/seed/createSeed");
    const { createForEachMockedTransaction } = await mockServerCode();
    const { getTrx, rollback, commit } = createForEachMockedTransaction();
    const seed = createSeed({
    community: {
    name: "test",
    slug: "test-server-pub",
    },
    users: {
    admin: {
    role: MemberRole.admin,
    },
    stageEditor: {
    role: MemberRole.contributor,
    },
    },
    pubFields: {
    Title: { schemaName: CoreSchemaType.String },
    Description: { schemaName: CoreSchemaType.String },
    "Some relation": { schemaName: CoreSchemaType.String, relation: true },
    "Another relation": { schemaName: CoreSchemaType.String, relation: true },
    },
    pubTypes: {
    "Basic Pub": {
    Title: { isTitle: true },
    "Some relation": { isTitle: false },
    "Another relation": { isTitle: false },
    },
    "Minimal Pub": {
    Title: { isTitle: true },
    },
    },
    stages: {
    "Stage 1": {
    members: {
    stageEditor: MemberRole.editor,
    },
    },
    "Stage 2": {
    members: {
    stageEditor: MemberRole.editor,
    },
    },
    },
    pubs: [
    {
    pubType: "Basic Pub",
    values: {
    Title: "Some title",
    },
    stage: "Stage 1",
    },
    {
    pubType: "Basic Pub",
    values: {
    Title: "Another title",
    },
    relatedPubs: {
    "Some relation": [
    {
    value: "test relation value",
    pub: {
    pubType: "Basic Pub",
    values: {
    Title: "A pub related to another Pub",
    },
    },
    },
    ],
    },
    },
    {
    stage: "Stage 1",
    pubType: "Minimal Pub",
    values: {
    Title: "Minimal pub",
    },
    },
    ],
    });
    let seededCommunity: CommunitySeedOutput<typeof seed>;
    beforeAll(async () => {
    const { seedCommunity } = await import("~/prisma/seed/seedCommunity");
    seededCommunity = await seedCommunity(seed);
    });
    describe("PubOp", () => {
    it("should create a new pub", async () => {
    const id = crypto.randomUUID() as PubsId;
    const pubOp = PubOp.upsert(id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    });
    const pub = await pubOp.execute();
    await expect(pub.id).toExist();
    });
    it("should not fail when upserting existing pub", async () => {
    const id = crypto.randomUUID() as PubsId;
    const pubOp = PubOp.upsert(id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    });
    const pub = await pubOp.execute();
    await expect(pub.id).toExist();
    const pub2 = await PubOp.upsert(id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).execute();
    await expect(pub2.id).toExist();
    });
    it("should create a new pub and set values", async () => {
    const id = crypto.randomUUID() as PubsId;
    const pubOp = PubOp.upsert(id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Some title")
    .set({
    [seededCommunity.pubFields["Description"].slug]: "Some description",
    });
    const pub = await pubOp.execute();
    await expect(pub.id).toExist();
    expect(pub).toHaveValues([
    {
    fieldSlug: seededCommunity.pubFields["Description"].slug,
    value: "Some description",
    },
    {
    fieldSlug: seededCommunity.pubFields["Title"].slug,
    value: "Some title",
    },
    ]);
    });
    it("should be able to relate existing pubs", async () => {
    const pubOp = PubOp.upsert(crypto.randomUUID() as PubsId, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    });
    const pub = await pubOp.execute();
    await expect(pub.id).toExist();
    const pub2 = await PubOp.upsert(crypto.randomUUID() as PubsId, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, "test relations value", pub.id)
    .execute();
    await expect(pub2.id).toExist();
    expect(pub2).toHaveValues([
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "test relations value",
    relatedPubId: pub.id,
    },
    ]);
    });
    it("should create multiple related pubs in a single operation", async () => {
    const mainPub = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main Pub")
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "the first related pub",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related Pub 1")
    )
    .relate(
    seededCommunity.pubFields["Another relation"].slug,
    "the second related pub",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related Pub 2")
    );
    const result = await mainPub.execute();
    expect(result).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Main Pub" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "the first related pub",
    relatedPubId: expect.any(String),
    },
    {
    fieldSlug: seededCommunity.pubFields["Another relation"].slug,
    value: "the second related pub",
    relatedPubId: expect.any(String),
    },
    ]);
    });
    it("should handle deeply nested relations", async () => {
    const relatedPub = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Level 1")
    .relate(
    seededCommunity.pubFields["Another relation"].slug,
    "the second related pub",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Level 2")
    );
    const mainPub = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Root")
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "the first related pub",
    relatedPub
    );
    const result = await mainPub.execute();
    expect(result).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Root" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "the first related pub",
    relatedPubId: expect.any(String),
    relatedPub: {
    values: [
    {
    fieldSlug: seededCommunity.pubFields["Title"].slug,
    value: "Level 1",
    },
    {
    fieldSlug: seededCommunity.pubFields["Another relation"].slug,
    value: "the second related pub",
    relatedPubId: expect.any(String),
    },
    ],
    },
    },
    ]);
    });
    it("should handle mixing existing and new pubs in relations", async () => {
    // First create a pub that we'll relate to
    const existingPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Existing Pub")
    .execute();
    const mainPub = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main Pub")
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "the first related pub",
    existingPub.id
    )
    .relate(
    seededCommunity.pubFields["Another relation"].slug,
    "the second related pub",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "New Related Pub")
    );
    const result = await mainPub.execute();
    expect(result).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Main Pub" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "the first related pub",
    relatedPubId: existingPub.id,
    relatedPub: {
    id: existingPub.id,
    values: [
    {
    fieldSlug: seededCommunity.pubFields["Title"].slug,
    value: "Existing Pub",
    },
    ],
    },
    },
    {
    fieldSlug: seededCommunity.pubFields["Another relation"].slug,
    value: "the second related pub",
    relatedPubId: expect.any(String),
    relatedPub: {
    values: [
    {
    fieldSlug: seededCommunity.pubFields["Title"].slug,
    value: "New Related Pub",
    },
    ],
    },
    },
    ]);
    });
    it("should handle circular relations", async () => {
    const pub1 = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Pub 1");
    const pub2 = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 2")
    .relate(seededCommunity.pubFields["Some relation"].slug, "the first related pub", pub1);
    pub1.relate(
    seededCommunity.pubFields["Another relation"].slug,
    "the second related pub",
    pub2
    );
    const result = await pub1.execute();
    expect(result).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 1" },
    {
    fieldSlug: seededCommunity.pubFields["Another relation"].slug,
    value: "the second related pub",
    relatedPubId: expect.any(String),
    relatedPub: {
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 2" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "the first related pub",
    relatedPubId: result.id,
    },
    ],
    },
    },
    ]);
    });
    it("should fail if you try to createWithId a pub that already exists", async () => {
    const pubOp = PubOp.createWithId(seededCommunity.pubs[0].id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    });
    await expect(pubOp.execute()).rejects.toThrow(
    /Cannot create a pub with an id that already exists/
    );
    });
    it("should update the value of a relationship", async () => {
    const pub1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1")
    .execute();
    const pub2 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 2")
    .relate(seededCommunity.pubFields["Some relation"].slug, "initial value", pub1.id)
    .execute();
    const updatedPub = await PubOp.upsert(pub2.id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, "updated value", pub1.id)
    .execute();
    expect(updatedPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 2" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "updated value",
    relatedPubId: pub1.id,
    },
    ]);
    });
    it("should be able to create a related pub with a different pubType then the toplevel pub", async () => {
    const pub1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1")
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "relation",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Minimal Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Pub 2")
    )
    .execute();
    expect(pub1.pubTypeId).toBe(seededCommunity.pubTypes["Basic Pub"].id);
    expect(pub1).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 1" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation",
    relatedPub: {
    pubTypeId: seededCommunity.pubTypes["Minimal Pub"].id,
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 2" },
    ],
    },
    },
    ]);
    });
    describe("upsert", () => {
    // when upserting a pub, we should (by default) delete existing values that are not being updated,
    // like a PUT
    it("should delete existing values that are not being updated", async () => {
    const pub1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1")
    .set(seededCommunity.pubFields["Description"].slug, "Description 1")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 1", (pubOp) =>
    pubOp
    .create({
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 2")
    )
    .execute();
    const upsertedPub = await PubOp.upsert(pub1.id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1, updated")
    .execute();
    expect(upsertedPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 1, updated" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation 1",
    relatedPubId: expect.any(String),
    relatedPub: {
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 2" },
    ],
    },
    },
    ]);
    });
    it("should not delete existing values if the `deleteExistingValues` option is false", async () => {
    const pub1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1")
    .set(seededCommunity.pubFields["Description"].slug, "Description 1")
    .execute();
    const upsertedPub = await PubOp.upsert(pub1.id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1, updated", {
    deleteExistingValues: false,
    })
    .execute();
    expect(upsertedPub).toHaveValues([
    {
    fieldSlug: seededCommunity.pubFields["Description"].slug,
    value: "Description 1",
    },
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 1, updated" },
    ]);
    });
    });
    });
    describe("relation management", () => {
    it("should disrelate a specific relation", async () => {
    const pub1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 1")
    .execute();
    const pub2 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Pub 2")
    .relate(seededCommunity.pubFields["Some relation"].slug, "initial value", pub1.id)
    .execute();
    // disrelate the relation
    const updatedPub = await PubOp.update(pub2.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .unrelate(seededCommunity.pubFields["Some relation"].slug, pub1.id)
    .execute();
    expect(updatedPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Pub 2" },
    ]);
    });
    it("should delete orphaned pubs when disrelateing relations", async () => {
    const orphanedPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Soon to be orphaned")
    .execute();
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main pub")
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "only relation",
    orphanedPub.id
    )
    .execute();
    // disrelate with deleteOrphaned option
    await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .unrelate(seededCommunity.pubFields["Some relation"].slug, orphanedPub.id, {
    deleteOrphaned: true,
    })
    .execute();
    await expect(orphanedPub.id).not.toExist();
    });
    it("should clear all relations for a specific field", async () => {
    const related1Id = crypto.randomUUID() as PubsId;
    const related2Id = crypto.randomUUID() as PubsId;
    const related1 = PubOp.createWithId(related1Id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related 1");
    const related2 = PubOp.createWithId(related2Id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related 2");
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main pub")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 1", related1)
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 2", related2)
    .execute();
    await expect(related1Id).toExist();
    await expect(related2Id).toExist();
    // clear all relations for the field
    const updatedPub = await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .unrelate(seededCommunity.pubFields["Some relation"].slug, "*", {
    deleteOrphaned: true,
    })
    .execute();
    expect(updatedPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Main pub" },
    ]);
    await expect(related1Id).not.toExist();
    await expect(related2Id).not.toExist();
    });
    it("should override existing relations when using override option", async () => {
    // Create initial related pubs
    const related1 = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related 1");
    const related2 = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "Related 2");
    // Create main pub with initial relations
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main pub")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 1", related1)
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 2", related2)
    .execute();
    const relatedPub1 = mainPub.values.find((v) => v.value === "relation 1")?.relatedPubId;
    const relatedPub2 = mainPub.values.find((v) => v.value === "relation 2")?.relatedPubId;
    expect(relatedPub1).toBeDefined();
    expect(relatedPub2).toBeDefined();
    await expect(relatedPub1).toExist();
    await expect(relatedPub2).toExist();
    const related3 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Related 3")
    .execute();
    // Update with override - only related3 should remain
    const updatedPub = await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, "new relation", related3.id, {
    replaceExisting: true,
    })
    .execute();
    expect(updatedPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Main pub" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "new relation",
    relatedPubId: related3.id,
    },
    ]);
    // related pubs should still exist
    await expect(relatedPub1).toExist();
    await expect(relatedPub2).toExist();
    });
    it("should handle multiple override relations for the same field", async () => {
    const related1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Related 1")
    .execute();
    const related2 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Related 2")
    .execute();
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main pub")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 1", related1.id, {
    replaceExisting: true,
    })
    .execute();
    const updatedMainPub = await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation 2", related2.id, {
    replaceExisting: true,
    })
    .relate(
    seededCommunity.pubFields["Some relation"].slug,
    "relation 3",
    PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }),
    { replaceExisting: true }
    )
    .execute();
    // Should have relation 2 and 3, but not 1
    expect(updatedMainPub).toHaveValues([
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Main pub" },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation 2",
    relatedPubId: related2.id,
    },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation 3",
    relatedPubId: expect.any(String),
    },
    ]);
    });
    it("should handle complex nested relation scenarios", async () => {
    const trx = getTrx();
    // manual rollback try/catch bc we are manually setting pubIds, so a failure in the middle of this will leave the db in a weird state
    try {
    // Create all pubs with meaningful IDs
    const pubA = "aaaaaaaa-0000-0000-0000-000000000000" as PubsId;
    const pubB = "bbbbbbbb-0000-0000-0000-000000000000" as PubsId;
    const pubC = "cccccccc-0000-0000-0000-000000000000" as PubsId;
    const pubD = "dddddddd-0000-0000-0000-000000000000" as PubsId;
    const pubE = "eeeeeeee-0000-0000-0000-000000000000" as PubsId;
    const pubF = "ffffffff-0000-0000-0000-000000000000" as PubsId;
    const pubG = "11111111-0000-0000-0000-000000000000" as PubsId;
    const pubH = "22222222-0000-0000-0000-000000000000" as PubsId;
    const pubI = "33333333-0000-0000-0000-000000000000" as PubsId;
    const pubJ = "44444444-0000-0000-0000-000000000000" as PubsId;
    const pubK = "55555555-0000-0000-0000-000000000000" as PubsId;
    const pubL = "66666666-0000-0000-0000-000000000000" as PubsId;
    // create the graph structure:
    // A J
    // / \ |
    // / \ |
    // B C --> I
    // | / \
    // G --> E D
    // / \
    // F H
    // / \
    // K --> L
    // create leaf nodes first
    const pubL_op = PubOp.createWithId(pubL, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    }).set(seededCommunity.pubFields["Title"].slug, "L");
    const pubK_op = PubOp.createWithId(pubK, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "K")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to L", pubL_op);
    const pubF_op = PubOp.createWithId(pubF, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    }).set(seededCommunity.pubFields["Title"].slug, "F");
    const pubH_op = PubOp.createWithId(pubH, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "H")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to K", pubK_op)
    .relate(seededCommunity.pubFields["Some relation"].slug, "to L", pubL_op);
    const pubE_op = PubOp.createWithId(pubE, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "E")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to F", pubF_op);
    const pubG_op = PubOp.createWithId(pubG, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "G")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to E", pubE_op);
    const pubD_op = PubOp.createWithId(pubD, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "D")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to H", pubH_op);
    const pubI_op = PubOp.createWithId(pubI, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    }).set(seededCommunity.pubFields["Title"].slug, "I");
    // Create second layer
    const pubB_op = PubOp.createWithId(pubB, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "B")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to G", pubG_op);
    const pubC_op = PubOp.createWithId(pubC, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "C")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to I", pubI_op)
    .relate(seededCommunity.pubFields["Some relation"].slug, "to D", pubD_op)
    .relate(seededCommunity.pubFields["Some relation"].slug, "to E", pubE_op);
    // create root and J
    const rootPub = await PubOp.createWithId(pubA, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "A")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to B", pubB_op)
    .relate(seededCommunity.pubFields["Some relation"].slug, "to C", pubC_op)
    .execute();
    const pubJ_op = await PubOp.createWithId(pubJ, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "J")
    .relate(seededCommunity.pubFields["Some relation"].slug, "to I", pubI)
    .execute();
    const { getPubsWithRelatedValuesAndChildren } = await import("~/lib/server/pub");
    // verify the initial state
    const initialState = await getPubsWithRelatedValuesAndChildren(
    {
    pubId: pubA,
    communityId: seededCommunity.community.id,
    },
    { trx, depth: 10 }
    );
    expect(initialState).toHaveValues([
    { value: "A" },
    {
    value: "to B",
    relatedPubId: pubB,
    relatedPub: {
    values: [
    { value: "B" },
    {
    value: "to G",
    relatedPubId: pubG,
    relatedPub: {
    values: [
    { value: "G" },
    {
    value: "to E",
    relatedPubId: pubE,
    relatedPub: {
    values: [
    { value: "E" },
    {
    value: "to F",
    relatedPubId: pubF,
    relatedPub: {
    values: [{ value: "F" }],
    },
    },
    ],
    },
    },
    ],
    },
    },
    ],
    },
    },
    {
    value: "to C",
    relatedPubId: pubC,
    relatedPub: {
    values: [
    { value: "C" },
    {
    value: "to D",
    relatedPubId: pubD,
    relatedPub: {
    values: [
    { value: "D" },
    {
    value: "to H",
    relatedPubId: pubH,
    relatedPub: {
    values: [
    { value: "H" },
    {
    value: "to K",
    relatedPubId: pubK,
    relatedPub: {
    values: [
    { value: "K" },
    {
    value: "to L",
    relatedPubId: pubL,
    relatedPub: {
    values: [{ value: "L" }],
    },
    },
    ],
    },
    },
    {
    value: "to L",
    },
    ],
    },
    },
    ],
    },
    },
    {
    value: "to E",
    relatedPubId: pubE,
    },
    {
    value: "to I",
    relatedPubId: pubI,
    relatedPub: {
    values: [{ value: "I" }],
    },
    },
    ],
    },
    },
    ]);
    // Now we disrelate C from A, which should
    // orphan everything from D down,
    // but should not orphan I, bc J still points to it
    // and should not orphan G, bc B still points to it
    // it orphans L, even though K points to it, because K is itself an orphan
    // A J
    // / |
    // v X v
    // B C --> I
    // | / \
    // v v v
    // G --> E D
    // | \
    // v v
    // F H
    // / \
    // v v
    // K --> L
    await PubOp.update(pubA, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .unrelate(seededCommunity.pubFields["Some relation"].slug, pubC, {
    deleteOrphaned: true,
    })
    .execute();
    // verify deletions
    await expect(pubA, "A should exist").toExist(trx);
    await expect(pubB, "B should exist").toExist(trx);
    await expect(pubC, "C should not exist").not.toExist(trx);
    await expect(pubD, "D should not exist").not.toExist(trx);
    await expect(pubE, "E should exist").toExist(trx); // still relateed through G
    await expect(pubF, "F should exist").toExist(trx); // still relateed through E
    await expect(pubG, "G should exist").toExist(trx); // not relateed to C at all
    await expect(pubH, "H should not exist").not.toExist(trx);
    await expect(pubI, "I should exist").toExist(trx); // still relateed through J
    await expect(pubJ, "J should exist").toExist(trx); // not relateed to C at all
    await expect(pubK, "K should not exist").not.toExist(trx);
    await expect(pubL, "L should not exist").not.toExist(trx);
    } catch (e) {
    rollback();
    throw e;
    }
    });
    it("should handle selective orphan deletion based on field", async () => {
    // Create a pub with two relations
    const related1 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Related 1")
    .execute();
    const related2 = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Related 2")
    .execute();
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation1", related1.id)
    .relate(seededCommunity.pubFields["Another relation"].slug, "relation2", related2.id)
    .execute();
    // clear one field with deleteOrphaned and one without
    await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .unrelate(seededCommunity.pubFields["Some relation"].slug, "*", {
    deleteOrphaned: true,
    })
    .unrelate(seededCommunity.pubFields["Another relation"].slug, "*")
    .execute();
    // related1 should be deleted (orphaned with deleteOrphaned: true)
    await expect(related1.id).not.toExist();
    // related2 should still exist (orphaned but deleteOrphaned not set)
    await expect(related2.id).toExist();
    });
    it("should handle override with mixed deleteOrphaned flags", async () => {
    // Create initial relations
    const toKeep = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Keep Me")
    .execute();
    const toDelete = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Delete Me")
    .execute();
    const mainPub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Main")
    .relate(seededCommunity.pubFields["Some relation"].slug, "keep", toKeep.id)
    .relate(seededCommunity.pubFields["Another relation"].slug, "delete", toDelete.id)
    .execute();
    // Override relations with different deleteOrphaned flags
    const newRelation = PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    }).set(seededCommunity.pubFields["Title"].slug, "New");
    await PubOp.update(mainPub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, "new", newRelation, {
    replaceExisting: true,
    })
    .relate(seededCommunity.pubFields["Another relation"].slug, "also new", newRelation, {
    replaceExisting: true,
    deleteOrphaned: true,
    })
    .execute();
    // toKeep should still exist (override without deleteOrphaned)
    await expect(toKeep.id).toExist();
    // toDelete should be deleted (override with deleteOrphaned)
    await expect(toDelete.id).not.toExist();
    });
    /**
    * this is so you do not need to keep specifying the communityId, pubTypeId, etc.
    * when creating nested PubOps
    */
    it("should be able to do PubOps inline in a relate", async () => {
    const pub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .set(seededCommunity.pubFields["Title"].slug, "Test")
    .relate(seededCommunity.pubFields["Some relation"].slug, "relation1", (pubOp) =>
    pubOp
    .create({ pubTypeId: seededCommunity.pubTypes["Minimal Pub"].id })
    .set(seededCommunity.pubFields["Title"].slug, "Relation 1")
    )
    .execute();
    expect(pub).toHaveValues([
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation1",
    relatedPub: {
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Relation 1" },
    ],
    },
    },
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Test" },
    ]);
    });
    it("should be able to relate many pubs at once", async () => {
    const pub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    })
    .relate(seededCommunity.pubFields["Some relation"].slug, [
    {
    target: (pubOp) =>
    pubOp
    .create({ pubTypeId: seededCommunity.pubTypes["Minimal Pub"].id })
    .set(seededCommunity.pubFields["Title"].slug, "Relation 1"),
    value: "relation1",
    },
    {
    target: (pubOp) =>
    pubOp
    .create({ pubTypeId: seededCommunity.pubTypes["Minimal Pub"].id })
    .set(seededCommunity.pubFields["Title"].slug, "Relation 2"),
    value: "relation2",
    },
    ])
    .execute();
    expect(pub).toHaveValues([
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation1",
    relatedPub: {
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Relation 1" },
    ],
    },
    },
    {
    fieldSlug: seededCommunity.pubFields["Some relation"].slug,
    value: "relation2",
    relatedPub: {
    values: [
    { fieldSlug: seededCommunity.pubFields["Title"].slug, value: "Relation 2" },
    ],
    },
    },
    ]);
    });
    });
    describe("PubOp stage", () => {
    it("should be able to set a stage while creating a pub", async () => {
    const trx = getTrx();
    const pub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "Test")
    .setStage(seededCommunity.stages["Stage 1"].id)
    .execute();
    expect(pub.stageId).toEqual(seededCommunity.stages["Stage 1"].id);
    });
    it("should be able to unset a stage", async () => {
    const trx = getTrx();
    const pub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .setStage(seededCommunity.stages["Stage 1"].id)
    .execute();
    expect(pub.stageId).toEqual(seededCommunity.stages["Stage 1"].id);
    const updatedPub = await PubOp.update(pub.id, {
    communityId: seededCommunity.community.id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .setStage(null)
    .execute();
    expect(updatedPub.stageId).toEqual(null);
    });
    it("should be able to move a pub to different stage", async () => {
    const trx = getTrx();
    const pub = await PubOp.create({
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .set(seededCommunity.pubFields["Title"].slug, "Test")
    .setStage(seededCommunity.stages["Stage 1"].id)
    .execute();
    const updatedPub = await PubOp.upsert(pub.id, {
    communityId: seededCommunity.community.id,
    pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
    lastModifiedBy: createLastModifiedBy("system"),
    trx,
    })
    .setStage(seededCommunity.stages["Stage 2"].id)
    .execute();
    expect(updatedPub.stageId).toEqual(seededCommunity.stages["Stage 2"].id);
    });
    });

  2. Write one extra test to get a feel for it (don't need to push it, but would be appreciated!)

Screenshots (if applicable)

Notes

Note

This new API isn't used anywhere yet, that comes later!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are helper methods to make matching pubvalues easier.

eg instead of doing

expect(pub.values).toHaveLength(3);
pub.values.sort(/* sorting bc you want the order of the values to be stable over different tests */)
expect(pub.values).toMatchObject([ 
	...
]);

you just do

expect(pub).toHaveValues([...])

Similarly for

expect(pubId).toExist()

just easier than manually looking up the pub yourself

Comment on lines +796 to +826
it("should handle complex nested relation scenarios", async () => {
const trx = getTrx();
// manual rollback try/catch bc we are manually setting pubIds, so a failure in the middle of this will leave the db in a weird state
try {
// Create all pubs with meaningful IDs
const pubA = "aaaaaaaa-0000-0000-0000-000000000000" as PubsId;
const pubB = "bbbbbbbb-0000-0000-0000-000000000000" as PubsId;
const pubC = "cccccccc-0000-0000-0000-000000000000" as PubsId;
const pubD = "dddddddd-0000-0000-0000-000000000000" as PubsId;
const pubE = "eeeeeeee-0000-0000-0000-000000000000" as PubsId;
const pubF = "ffffffff-0000-0000-0000-000000000000" as PubsId;
const pubG = "11111111-0000-0000-0000-000000000000" as PubsId;
const pubH = "22222222-0000-0000-0000-000000000000" as PubsId;
const pubI = "33333333-0000-0000-0000-000000000000" as PubsId;
const pubJ = "44444444-0000-0000-0000-000000000000" as PubsId;
const pubK = "55555555-0000-0000-0000-000000000000" as PubsId;
const pubL = "66666666-0000-0000-0000-000000000000" as PubsId;

// create the graph structure:
// A J
// / \ |
// / \ |
// B C --> I
// | / \
// G --> E D
// / \
// F H
// / \
// K --> L

// create leaf nodes first
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is by far the most complex test, and very curious whether you agree with whether this should be the behavior.
i spent a bit too long trying to figure this out haha

see my comment in pub-op.ts for a more in depth explanation about the "algorithm" for determining orphans

Comment on lines +833 to +999
*
* Notably, E and I are not deleted, because
* 1. E is the target of a relation from G, which, while still a relation itself, is not reachable from the C-tree
* 2. I is the target of a relation from J, which, while still a relation itself, is not reachable from the C-tree
*
* So this should be the resulting graph:
*
* ```
* A J
* ┌──┴ │
* ▼ ▼
* B I
* │
* ▼
* G ─► E
* │
* ▼
* F
* ```
*
*
*/
private async cleanupOrphanedPubs(
trx: Transaction<Database>,
orphanedPubIds: PubsId[]
): Promise<void> {
if (orphanedPubIds.length === 0) {
return;
}

const pubsToDelete = await trx
.withRecursive("affected_pubs", (db) => {
// Base case: direct connections from the to-be-removed-pubs down
const initial = db
.selectFrom("pub_values")
.select(["pubId as id", sql<string[]>`array["pubId"]`.as("path")])
.where("pubId", "in", orphanedPubIds);

// Recursive case: keep traversing outward
const recursive = db
.selectFrom("pub_values")
.select([
"relatedPubId as id",
sql<string[]>`affected_pubs.path || array["relatedPubId"]`.as("path"),
])
.innerJoin("affected_pubs", "pub_values.pubId", "affected_pubs.id")
.where((eb) => eb.not(eb("relatedPubId", "=", eb.fn.any("affected_pubs.path")))) // Prevent cycles
.$narrowType<{ id: PubsId }>();

return initial.union(recursive);
})
// pubs in the affected_pubs table but which should not be deleted because they are still related to other pubs
.with("safe_pubs", (db) => {
return (
db
.selectFrom("pub_values")
.select(["relatedPubId as id"])
.distinct()
// crucial part:
// find all the pub_values which
// - point to a node in the affected_pubs
// - but are not themselves affected
// these are the "safe" nodes
.innerJoin("affected_pubs", "pub_values.relatedPubId", "affected_pubs.id")
.where((eb) =>
eb.not(
eb.exists((eb) =>
eb
.selectFrom("affected_pubs")
.select("id")
.whereRef("id", "=", "pub_values.pubId")
)
)
)
);
})
.selectFrom("affected_pubs")
.select(["id", "path"])
.distinctOn("id")
.where((eb) =>
eb.not(
eb.exists((eb) =>
eb
.selectFrom("safe_pubs")
.select("id")
.where(sql<boolean>`safe_pubs.id = any(affected_pubs.path)`)
)
)
)
.execute();

if (pubsToDelete.length > 0) {
await deletePub({
pubId: pubsToDelete.map((p) => p.id),
communityId: this.options.communityId,
lastModifiedBy: this.options.lastModifiedBy,
trx,
});
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is by far the most complex part of the PR, am curious if you think the general approach is correct! obviously very annoying to check the actual sql, but im more curious whether you think my definition of orphans here tracks

Comment on lines +631 to +640
protected async executeWithTrx(trx: Transaction<Database>): Promise<PubsId> {
const operations = this.collectOperations();

await this.createAllPubs(trx, operations);
await this.processStages(trx, operations);
await this.processRelations(trx, operations);
await this.processValues(trx, operations);

return this.id;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically entirely what a pubOp can do. First create all pubs, add them to stages, do relation stuff, then do value stuff (those last two might be able to be done at the same time, or changed)

notably, all of these things happen at the same time for all pubs. so no more recursive calls, we first collect all operations and then do these things one by one. much more easy to reason about that way i think

Comment on lines +1098 to +1130
if (nullStages.length > 0) {
await autoRevalidate(
trx.deleteFrom("PubsInStages").where(
"pubId",
"in",
nullStages.map(({ pubId }) => pubId)
)
).execute();
}

const nonNullStages = stagesToUpdate.filter(({ stageId }) => stageId !== null);

if (nonNullStages.length > 0) {
await autoRevalidate(
trx
.with("deletedStages", (db) =>
db
.deleteFrom("PubsInStages")
.where((eb) =>
eb.or(
nonNullStages.map((stageOp) => eb("pubId", "=", stageOp.pubId))
)
)
)
.insertInto("PubsInStages")
.values(
nonNullStages.map((stageOp) => ({
pubId: stageOp.pubId,
stageId: stageOp.stageId,
}))
)
).execute();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully thisll be a bit simpler once #967 lands!

Copy link
Member Author

@tefkah tefkah Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seperated this out into a separate file so you can createSeed while still having the automatic rollbacks work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah missed this earlier, awesome!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please look here for usage!
im really curious what you all think. please comment any ideas/feedback you have for better naming or anything else regarding the api! really interested in anything, especially "i hate this" or something!

@tefkah tefkah changed the title dev: pubOp for creating and updating pubs more easily RFC: pubOp for creating and updating pubs more easily Feb 11, 2025
@tefkah tefkah marked this pull request as ready for review February 11, 2025 18:40
@@ -2,12 +2,12 @@ import { describe, expect, it } from "vitest";

import { CoreSchemaType, MemberRole } from "db/public";

import type { Seed } from "~/prisma/seed/seedCommunity";
import { createSeed } from "~/prisma/seed/createSeed";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc importing createSeed up here did make data persist whereas importing just the type { Seed } didn't?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but i think that was bc they were both imported from /seedCommunity. I separated out the createSeed to a different file, which (I did not explicitly check) i think should fix that issue.
i do think using createSeed is slightly better as that preserves the type info once you pass it to seedCommunity, eg if you have pubTypes: { 'SomepubType': ... } in your seed, the output of seedcommunity will have pubTypes.SomePubType as well

expected: Partial<ProcessedPub["values"][number]>[]
) {
if (typeof received === "string") {
throw new Error("toHaveValues() can only be called with a ProcessedPub");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come received: PubsId | ProcessedPub up above instead of received: ProcessedPub then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! it was left over bc i think i wanted to be able to pass in both in either case, and was having some trouble with the types. i later decided to just use PubsId for toExist and ProcessedPub for toHaveValues, but never updated it! I'll fix it

Comment on lines +172 to +178
const pub2 = await PubOp.upsert(crypto.randomUUID() as PubsId, {
communityId: seededCommunity.community.id,
pubTypeId: seededCommunity.pubTypes["Basic Pub"].id,
lastModifiedBy: createLastModifiedBy("system"),
})
.relate(seededCommunity.pubFields["Some relation"].slug, "test relations value", pub.id)
.execute();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is quite nice! just so I understand, if pub2 were already created, would we also have to call .upsert(...).relate(...)? i.e. there's nothing like PubOp.relate

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah correct!
You'd more likely do PubOp.update(pub2Id).relate() if you did not want to override the existing relations, but yes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for context, initially i did only have upsert, not create and update, but i thought it would be best to have separate ones bc

  • pubOp.upsert().unrelate() is kind of weird I think
  • I wanted to have upsert more act like a PUT than a PATCH if the pub already exists, ie get rid of the existing values/relations by default if they are not mentioned in the upsert. But i ofc did want to keep the functionality to only update a subset of values, so we would need an update (or force consumers of this api to do a bunch of unnatural configuration, which is what i was doing before in feat: add proper pub upsert functions #914 )
  • it's good to get an error if you are explicitly trying to create a pub that already exists, rather than silently update it when that's maybe not what you wanted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks for the context, makes sense!

Comment on lines +787 to +790
/**
* remove pubs that have been disconnected/their value removed,
* has `deleteOrphaned` set to true for their relevant relation operation,
* AND have no other relations
Copy link
Member Author

@tefkah tefkah Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it's a better idea to have this be a configuration setting on the PubType (or the pubField?)
like, for some relations this could be desireable, like versions and discussions in the current arcadia community. you probably want to delete all the versions of the pub if that pub is deleted.

but for others, like tag or something, you would not want this. you wouldn't want to delete a Tag pub if the you delete the last Pub that is relating to it, you probably want to keep it around.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the UI i think we can just force the user to manually delete all the relations first, but i think it would be good in certain places to automatically decide this (import actions)

lastModifiedBy: createLastModifiedBy("system"),
})
.set(seededCommunity.pubFields["Title"].slug, "Test")
.relate(seededCommunity.pubFields["Some relation"].slug, "relation1", (pubOp) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's really nice! 🙌

Copy link
Member

@3mcd 3mcd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests look like they cover most of the PubOp behavior and the logic for pub orphans seems sound. Looking forward to tomorrow's chat about the recursive CTE.

@3mcd 3mcd merged commit 012772d into main Feb 12, 2025
6 checks passed
@3mcd 3mcd deleted the tfk/pub-op branch February 12, 2025 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants