Skip to content

[Feat] Type safe helpers for patch remove linked Ids #735

@luchillo17

Description

@luchillo17

This relates mostly to deleting 1 to 1 linked documents & files, but would be nice to have type safe helpers like these ones:

/**
 * Reusable helper type: keys in T whose values are assignable to V
 * @example
 * type A = KeysOfType<{ a: string; b: number }, string>; // 'a'
 * type B = KeysOfType<{ a: string; b: number }, number>; // 'b'
 * type C = KeysOfType<{ a: string; b: number }, string | number>; // 'a' | 'b'
 */
export type KeysOfType<T, V> = {
  [K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];

export async function deleteRemovedReferences<
  T extends Doc<TableNames>,
  K extends KeysOfType<T, Id<TableNames> | undefined>,
>(ctx: MutationCtx, oldDoc: T, newDoc: Partial<T>, idFields: K[]) {
  for (const field of idFields) {
    const oldId = oldDoc?.[field] as Id<TableNames> | undefined;
    const newId = newDoc?.[field] as Id<TableNames> | undefined;

    if (oldId && !newId) {
      await ctx.db.delete(oldId);
    }
  }
}

export async function deleteRemovedFileReferences<
  T extends Doc<TableNames>,
  K extends KeysOfType<T, Id<'files'> | undefined>,
>(ctx: MutationCtx, oldDoc: T, newDoc: Partial<T>, fileFields: K[]) {
  const idsToRemove: Id<'files'>[] = fileFields
    .filter((field) => oldDoc?.[field] && !newDoc?.[field])
    .map((field) => oldDoc?.[field] as Id<'files'>);

  if (idsToRemove.length > 0) {
    await ctx.runMutation(api.files.remove, {
      ids: idsToRemove,
    });
  }
}

And here's how these are used:

export const upsertDoctorProfile = mutation({
  args: { payload: v.any() },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Not authenticated');

    const parsed = DoctorProfileSchema.pipe(
      DoctorProfileFilesNullToUndefinedTransform,
    ).parse(args.payload);

    const existing = await ctx.db
      .query('doctorProfiles')
      .withIndex('by_user', (q) => q.eq('userId', identity.subject))
      .unique();

    if (!existing) {
      await ctx.db.insert(
        'doctorProfiles',
        addUpdatedAt({ userId: identity.subject, ...parsed }),
      );
      return true;
    }

    // Determine files to remove before patching
    await deleteRemovedFileReferences(ctx, existing, parsed, [
      'idCardFileId',
      'diplomaFileId',
      'graduationCertificateFileId',
      'rethusCertificateFileId',
      'homologationCertificateFileId',
      'habilitationBadgeFileId',
    ]);

    await ctx.db.patch(existing._id, addUpdatedAt({ ...parsed }));
    return true;
  },
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions