Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 3 additions & 27 deletions packages/common/src/services/decision/createProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
proposalAttachments,
proposalCategories,
proposals,
taxonomyTerms,
} from '@op/db/schema';
import type { User } from '@op/supabase/lib';
import { assertAccess, permission } from 'access-zones';
Expand All @@ -26,6 +25,7 @@ import { generateUniqueProfileSlug } from '../profile/utils';
import { processProposalContent } from './proposalContentProcessor';
import type { ProposalDataInput } from './proposalDataSchema';
import type { DecisionInstanceData } from './schemas/instanceData';
import { ensureProposalTaxonomyTerm } from './utils/ensureProposalTaxonomyTerm';
import { checkProposalsAllowed } from './utils/proposal';

export interface CreateProposalInput {
Expand Down Expand Up @@ -85,33 +85,9 @@ export const createProposal = async ({
// Extract title from proposal data
const proposalTitle = extractTitleFromProposalData(data.proposalData);

// Pre-fetch category term if specified to avoid lookup inside transaction
// Ensure category taxonomy term exists (creates if missing)
const categoryLabel = (data.proposalData as any)?.category;
let categoryTermId: string | null = null;

if (categoryLabel?.trim()) {
try {
const taxonomyTerm = await db._query.taxonomyTerms.findFirst({
where: eq(taxonomyTerms.label, categoryLabel.trim()),
with: {
taxonomy: true,
},
});

if (taxonomyTerm && taxonomyTerm.taxonomy?.name === 'proposal') {
categoryTermId = taxonomyTerm.id;
} else {
console.warn(
`No valid proposal taxonomy term found for category: ${categoryLabel}`,
);
}
} catch (error) {
console.warn(
'Error fetching category term, proceeding without category:',
error,
);
}
}
const categoryTermId = await ensureProposalTaxonomyTerm(categoryLabel);

const profileId = await getCurrentProfileId(authUserId);
const proposal = await db.transaction(async (tx) => {
Expand Down
37 changes: 9 additions & 28 deletions packages/common/src/services/decision/updateProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
processInstances,
proposalCategories,
proposals,
taxonomies,
taxonomyTerms,
} from '@op/db/schema';
import { User } from '@op/supabase/lib';
import { checkPermission, permission } from 'access-zones';
Expand All @@ -24,6 +22,7 @@ import { getOrgAccessUser } from '../access';
import { assertUserByAuthId } from '../assert';
import type { ProposalDataInput } from './proposalDataSchema';
import { schemaValidator } from './schemaValidator';
import { ensureProposalTaxonomyTerm } from './utils/ensureProposalTaxonomyTerm';

type ProcessInstanceWithProcess = ProcessInstance & {
process: DecisionProcess;
Expand All @@ -42,34 +41,16 @@ async function updateProposalCategoryLink(
.delete(proposalCategories)
.where(eq(proposalCategories.proposalId, proposalId));

// Add new category link if provided
if (newCategoryLabel?.trim()) {
// Find the "proposal" taxonomy
const proposalTaxonomy = await db._query.taxonomies.findFirst({
where: eq(taxonomies.name, 'proposal'),
});

if (!proposalTaxonomy) {
console.warn('No "proposal" taxonomy found, skipping category linking');
return;
}
// Add new category link if provided (creates term if missing)
const categoryTermId = await ensureProposalTaxonomyTerm(
newCategoryLabel ?? '',
);

// Find the taxonomy term that matches the category label
const taxonomyTerm = await db._query.taxonomyTerms.findFirst({
where: eq(taxonomyTerms.label, newCategoryLabel.trim()),
if (categoryTermId) {
await db.insert(proposalCategories).values({
proposalId,
taxonomyTermId: categoryTermId,
});

if (taxonomyTerm) {
// Create the new link
await db.insert(proposalCategories).values({
proposalId,
taxonomyTermId: taxonomyTerm.id,
});
} else {
console.warn(
`No taxonomy term found for category: ${newCategoryLabel}`,
);
}
}
} catch (error) {
console.error('Error updating proposal category link:', error);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { db, eq } from '@op/db/client';
import { taxonomies, taxonomyTerms } from '@op/db/schema';

import { CommonError } from '../../../utils';

/**
* Finds or creates a taxonomy term for a proposal category.
* Ensures the "proposal" taxonomy exists, then looks up the term by label.
* If the term doesn't exist, creates it with a normalized URI.
*
* Returns the taxonomyTermId, or null if the label is empty/whitespace.
*/
export async function ensureProposalTaxonomyTerm(
categoryLabel: string,
): Promise<string | null> {
const trimmed = categoryLabel?.trim();
if (!trimmed) {
return null;
}

// Ensure "proposal" taxonomy exists
let proposalTaxonomy = await db._query.taxonomies.findFirst({
where: eq(taxonomies.name, 'proposal'),
});

if (!proposalTaxonomy) {
const [newTaxonomy] = await db
.insert(taxonomies)
.values({
name: 'proposal',
description:
'Categories for organizing proposals in decision-making processes',
})
.returning();

if (!newTaxonomy) {
throw new CommonError('Failed to create proposal taxonomy');
}
proposalTaxonomy = newTaxonomy;
}

// Look up existing term by label within the proposal taxonomy
const existingTerm = await db._query.taxonomyTerms.findFirst({
where: eq(taxonomyTerms.label, trimmed),
with: {
taxonomy: true,
},
});

if (existingTerm && existingTerm.taxonomy?.name === 'proposal') {
return existingTerm.id;
}

// Create a new term
const termUri = trimmed
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '');

const [newTerm] = await db
.insert(taxonomyTerms)
.values({
taxonomyId: proposalTaxonomy.id,
termUri,
label: trimmed,
definition: `Category for ${trimmed} proposals`,
})
.returning();

if (!newTerm) {
throw new CommonError(
`Failed to create taxonomy term for category: ${trimmed}`,
);
}

return newTerm.id;
}
Loading