task p-4.5
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import {
|
||||
buildSupportedMappingsForFieldNames,
|
||||
bumpMinorTemplateVersion,
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
getEffectiveMappedKeys,
|
||||
loadTemplateSourceByOrganizationName,
|
||||
summarizeIssues,
|
||||
type TemplateMappingSeed
|
||||
type TemplateMappingSeed,
|
||||
type TemplateSourceVariant,
|
||||
} from './pdf-audit-utils.ts';
|
||||
|
||||
type TemplateRow = {
|
||||
@@ -31,11 +32,16 @@ type TemplateVersionRow = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
function computeSchemaHash(schemaJson: unknown) {
|
||||
type ScriptOptions = {
|
||||
variant: TemplateSourceVariant;
|
||||
activate: boolean;
|
||||
};
|
||||
|
||||
function computeSchemaHash(schemaJson: unknown): string {
|
||||
return createHash('sha256').update(JSON.stringify(schemaJson)).digest('hex');
|
||||
}
|
||||
|
||||
function normalizeSchemaJson(schemaJson: unknown) {
|
||||
function normalizeSchemaJson(schemaJson: unknown): unknown {
|
||||
if (typeof schemaJson === 'string') {
|
||||
return JSON.parse(schemaJson) as unknown;
|
||||
}
|
||||
@@ -43,22 +49,66 @@ function normalizeSchemaJson(schemaJson: unknown) {
|
||||
return schemaJson;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ScriptOptions {
|
||||
let variant: TemplateSourceVariant = 'legacy';
|
||||
let activate = false;
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg === '--activate') {
|
||||
activate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--template-variant=')) {
|
||||
const value = arg.slice('--template-variant='.length);
|
||||
|
||||
if (value === 'legacy' || value === 'product-v1') {
|
||||
variant = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (variant === 'legacy') {
|
||||
activate = true;
|
||||
}
|
||||
|
||||
return { variant, activate };
|
||||
}
|
||||
|
||||
function resolveVersionLabel(args: {
|
||||
variant: TemplateSourceVariant;
|
||||
versions: TemplateVersionRow[];
|
||||
activeVersion: TemplateVersionRow;
|
||||
matchingVersion: TemplateVersionRow | null;
|
||||
}): string {
|
||||
if (args.matchingVersion) {
|
||||
return args.matchingVersion.version;
|
||||
}
|
||||
|
||||
if (args.variant === 'product-v1') {
|
||||
return '2.0';
|
||||
}
|
||||
|
||||
return bumpMinorTemplateVersion(args.versions.at(-1)?.version ?? args.activeVersion.version);
|
||||
}
|
||||
|
||||
async function upsertMapping(
|
||||
sql: any,
|
||||
organizationId: string,
|
||||
templateVersionId: string,
|
||||
mapping: TemplateMappingSeed
|
||||
mapping: TemplateMappingSeed,
|
||||
) {
|
||||
const existing = (
|
||||
(await sql`
|
||||
await sql`
|
||||
select id
|
||||
from crm_document_template_mappings
|
||||
where template_version_id = ${templateVersionId}
|
||||
and placeholder_key = ${mapping.placeholderKey}
|
||||
limit 1
|
||||
`) as Array<{ id: string }>
|
||||
)[0] ?? null;
|
||||
const mappingId = String(existing?.id ?? crypto.randomUUID());
|
||||
`
|
||||
)[0] as { id: string } | undefined;
|
||||
|
||||
const mappingId = existing?.id ?? randomUUID();
|
||||
|
||||
if (existing) {
|
||||
await sql`
|
||||
@@ -87,8 +137,7 @@ async function upsertMapping(
|
||||
sheet_name,
|
||||
default_value,
|
||||
format_mask,
|
||||
sort_order,
|
||||
deleted_at
|
||||
sort_order
|
||||
) values (
|
||||
${mappingId},
|
||||
${organizationId},
|
||||
@@ -99,8 +148,7 @@ async function upsertMapping(
|
||||
${null},
|
||||
${mapping.defaultValue ?? null},
|
||||
${mapping.formatMask ?? null},
|
||||
${mapping.sortOrder},
|
||||
${null}
|
||||
${mapping.sortOrder}
|
||||
)
|
||||
`;
|
||||
}
|
||||
@@ -108,16 +156,18 @@ async function upsertMapping(
|
||||
const desiredColumnNames = new Set((mapping.columns ?? []).map((column) => column.columnName));
|
||||
|
||||
for (const column of mapping.columns ?? []) {
|
||||
const existingColumn =
|
||||
(
|
||||
(await sql`
|
||||
select id
|
||||
from crm_document_template_table_columns
|
||||
where mapping_id = ${mappingId}
|
||||
and column_name = ${column.columnName}
|
||||
limit 1
|
||||
`) as Array<{ id: string }>
|
||||
)[0] ?? null;
|
||||
const existingColumn = (
|
||||
await sql`
|
||||
select id
|
||||
from crm_document_template_table_columns
|
||||
where mapping_id = ${mappingId}
|
||||
and column_name = ${column.columnName}
|
||||
and deleted_at is null
|
||||
limit 1
|
||||
`
|
||||
)[0] as { id: string } | undefined;
|
||||
|
||||
const columnId = existingColumn?.id ?? randomUUID();
|
||||
|
||||
if (existingColumn) {
|
||||
await sql`
|
||||
@@ -126,46 +176,43 @@ async function upsertMapping(
|
||||
organization_id = ${organizationId},
|
||||
source_field = ${column.sourceField},
|
||||
column_letter = ${column.columnLetter ?? null},
|
||||
sort_order = ${column.sortOrder},
|
||||
sort_order = ${column.sortOrder ?? 0},
|
||||
format_mask = ${column.formatMask ?? null},
|
||||
deleted_at = ${null},
|
||||
updated_at = now()
|
||||
where id = ${existingColumn.id}
|
||||
where id = ${columnId}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into crm_document_template_table_columns (
|
||||
id,
|
||||
organization_id,
|
||||
mapping_id,
|
||||
column_name,
|
||||
source_field,
|
||||
column_letter,
|
||||
sort_order,
|
||||
format_mask
|
||||
) values (
|
||||
${columnId},
|
||||
${organizationId},
|
||||
${mappingId},
|
||||
${column.columnName},
|
||||
${column.sourceField},
|
||||
${column.columnLetter ?? null},
|
||||
${column.sortOrder ?? 0},
|
||||
${column.formatMask ?? null}
|
||||
)
|
||||
`;
|
||||
continue;
|
||||
}
|
||||
|
||||
await sql`
|
||||
insert into crm_document_template_table_columns (
|
||||
id,
|
||||
organization_id,
|
||||
mapping_id,
|
||||
column_name,
|
||||
source_field,
|
||||
column_letter,
|
||||
sort_order,
|
||||
format_mask,
|
||||
deleted_at
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organizationId},
|
||||
${mappingId},
|
||||
${column.columnName},
|
||||
${column.sourceField},
|
||||
${column.columnLetter ?? null},
|
||||
${column.sortOrder},
|
||||
${column.formatMask ?? null},
|
||||
${null}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
const existingColumns = (await sql`
|
||||
const existingColumns = await sql`
|
||||
select id, column_name as "columnName"
|
||||
from crm_document_template_table_columns
|
||||
where mapping_id = ${mappingId}
|
||||
and deleted_at is null
|
||||
`) as Array<{ id: string; columnName: string }>;
|
||||
` as Array<{ id: string; columnName: string }>;
|
||||
|
||||
for (const column of existingColumns) {
|
||||
if (desiredColumnNames.has(column.columnName)) {
|
||||
@@ -174,19 +221,18 @@ async function upsertMapping(
|
||||
|
||||
await sql`
|
||||
update crm_document_template_table_columns
|
||||
set
|
||||
deleted_at = now(),
|
||||
updated_at = now()
|
||||
set deleted_at = now(), updated_at = now()
|
||||
where id = ${column.id}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const sql = createSqlClient();
|
||||
|
||||
try {
|
||||
const templates = (await sql`
|
||||
const templates = await sql`
|
||||
select
|
||||
t.id,
|
||||
t.organization_id as "organizationId",
|
||||
@@ -195,30 +241,33 @@ async function main() {
|
||||
t.created_by as "createdBy",
|
||||
t.updated_by as "updatedBy"
|
||||
from crm_document_templates t
|
||||
inner join organizations o
|
||||
on o.id = t.organization_id
|
||||
inner join organizations o on o.id = t.organization_id
|
||||
where t.document_type = 'quotation'
|
||||
and t.file_type = 'pdfme'
|
||||
and t.product_type = 'default'
|
||||
and t.deleted_at is null
|
||||
order by o.name asc, t.created_at asc
|
||||
`) as TemplateRow[];
|
||||
const results = [];
|
||||
` as TemplateRow[];
|
||||
|
||||
const results: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const template of templates) {
|
||||
const templateSource = loadTemplateSourceByOrganizationName(template.organizationName);
|
||||
const templateSource = loadTemplateSourceByOrganizationName(
|
||||
template.organizationName,
|
||||
options.variant,
|
||||
);
|
||||
|
||||
if (!templateSource) {
|
||||
results.push({
|
||||
organizationName: template.organizationName,
|
||||
templateName: template.templateName,
|
||||
status: 'WARNING',
|
||||
message: 'No source template file found for organization'
|
||||
message: 'No source template file found for organization.',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const versions = (await sql`
|
||||
const versions = await sql`
|
||||
select
|
||||
id,
|
||||
template_id as "templateId",
|
||||
@@ -231,32 +280,37 @@ async function main() {
|
||||
where template_id = ${template.id}
|
||||
and deleted_at is null
|
||||
order by created_at asc
|
||||
`) as TemplateVersionRow[];
|
||||
` as TemplateVersionRow[];
|
||||
|
||||
const activeVersion = versions.find((version) => version.isActive) ?? versions.at(-1) ?? null;
|
||||
|
||||
if (!activeVersion) {
|
||||
throw new Error(`Template ${template.templateName} has no version rows`);
|
||||
throw new Error(`Template ${template.templateName} has no version rows.`);
|
||||
}
|
||||
|
||||
const targetSchema = templateSource.schemaJson;
|
||||
const targetSchemaHash = computeSchemaHash(targetSchema);
|
||||
const matchingVersion =
|
||||
versions.find(
|
||||
(version) => computeSchemaHash(normalizeSchemaJson(version.schemaJson)) === targetSchemaHash
|
||||
) ?? null;
|
||||
const matchingVersion = versions.find(
|
||||
(version) => computeSchemaHash(normalizeSchemaJson(version.schemaJson)) === targetSchemaHash,
|
||||
) ?? null;
|
||||
const previousFields = extractTemplateFields(normalizeSchemaJson(activeVersion.schemaJson));
|
||||
const targetFields = extractTemplateFields(targetSchema);
|
||||
const targetFieldNames = new Set(targetFields.map((field) => field.name).filter(Boolean));
|
||||
const targetTokens = new Set(targetFields.flatMap((field) => field.tokens));
|
||||
const supportedMappings = buildSupportedMappingsForFieldNames(targetFieldNames, targetTokens);
|
||||
const supportedMappings = buildSupportedMappingsForFieldNames(
|
||||
targetFieldNames,
|
||||
targetTokens,
|
||||
);
|
||||
const mappedKeys = getEffectiveMappedKeys(targetFields, supportedMappings);
|
||||
const fieldDiff = computeFieldDiff(targetFields, previousFields);
|
||||
const createdNewVersion = !matchingVersion;
|
||||
const targetVersionLabel = matchingVersion
|
||||
? matchingVersion.version
|
||||
: bumpMinorTemplateVersion(versions.at(-1)?.version ?? activeVersion.version);
|
||||
|
||||
const targetVersionId = matchingVersion?.id ?? crypto.randomUUID();
|
||||
const targetVersionLabel = resolveVersionLabel({
|
||||
variant: options.variant,
|
||||
versions,
|
||||
activeVersion,
|
||||
matchingVersion,
|
||||
});
|
||||
const targetVersionId = matchingVersion?.id ?? randomUUID();
|
||||
|
||||
await sql.begin(async (tx) => {
|
||||
if (!matchingVersion) {
|
||||
@@ -280,7 +334,7 @@ async function main() {
|
||||
${templateSource.relativePath},
|
||||
${JSON.stringify(targetSchema)},
|
||||
${null},
|
||||
${true},
|
||||
${options.activate},
|
||||
${null},
|
||||
${template.updatedBy || template.createdBy}
|
||||
)
|
||||
@@ -290,32 +344,34 @@ async function main() {
|
||||
update crm_document_template_versions
|
||||
set
|
||||
file_path = ${templateSource.relativePath},
|
||||
is_active = ${true},
|
||||
is_active = ${options.activate ? true : matchingVersion.isActive},
|
||||
updated_at = now()
|
||||
where id = ${targetVersionId}
|
||||
`;
|
||||
}
|
||||
|
||||
await tx`
|
||||
update crm_document_template_versions
|
||||
set
|
||||
is_active = case when id = ${targetVersionId} then true else false end,
|
||||
updated_at = now()
|
||||
where template_id = ${template.id}
|
||||
and deleted_at is null
|
||||
`;
|
||||
if (options.activate) {
|
||||
await tx`
|
||||
update crm_document_template_versions
|
||||
set
|
||||
is_active = case when id = ${targetVersionId} then true else false end,
|
||||
updated_at = now()
|
||||
where template_id = ${template.id}
|
||||
and deleted_at is null
|
||||
`;
|
||||
}
|
||||
|
||||
for (const mapping of supportedMappings) {
|
||||
await upsertMapping(tx, template.organizationId, targetVersionId, mapping);
|
||||
}
|
||||
|
||||
const desiredKeys = new Set(supportedMappings.map((mapping) => mapping.placeholderKey));
|
||||
const existingMappings = (await tx`
|
||||
const existingMappings = await tx`
|
||||
select id, placeholder_key as "placeholderKey"
|
||||
from crm_document_template_mappings
|
||||
where template_version_id = ${targetVersionId}
|
||||
and deleted_at is null
|
||||
`) as Array<{ id: string; placeholderKey: string }>;
|
||||
` as Array<{ id: string; placeholderKey: string }>;
|
||||
|
||||
for (const mapping of existingMappings) {
|
||||
if (desiredKeys.has(mapping.placeholderKey)) {
|
||||
@@ -324,9 +380,7 @@ async function main() {
|
||||
|
||||
await tx`
|
||||
update crm_document_template_mappings
|
||||
set
|
||||
deleted_at = now(),
|
||||
updated_at = now()
|
||||
set deleted_at = now(), updated_at = now()
|
||||
where id = ${mapping.id}
|
||||
`;
|
||||
}
|
||||
@@ -338,16 +392,18 @@ async function main() {
|
||||
const classification = classifyTemplateField(field, mappedKeys);
|
||||
classificationCounts.set(
|
||||
classification,
|
||||
(classificationCounts.get(classification) ?? 0) + 1
|
||||
(classificationCounts.get(classification) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
results.push({
|
||||
organizationName: template.organizationName,
|
||||
templateName: template.templateName,
|
||||
variant: options.variant,
|
||||
activated: options.activate,
|
||||
sourceFile: templateSource.relativePath,
|
||||
previousActiveVersion: activeVersion.version,
|
||||
activeVersion: targetVersionLabel,
|
||||
targetVersion: targetVersionLabel,
|
||||
createdNewVersion,
|
||||
retainedInactiveVersions: versions.filter((version) => version.id !== targetVersionId).length,
|
||||
deletedFields: fieldDiff.deletedFields,
|
||||
@@ -355,14 +411,19 @@ async function main() {
|
||||
mappingKeys: supportedMappings.map((mapping) => mapping.placeholderKey),
|
||||
classificationCounts: Object.fromEntries(classificationCounts.entries()),
|
||||
status: summarizeIssues([
|
||||
fieldDiff.deletedFields.includes('exclusion_data') ? 'PASS' : 'PASS',
|
||||
supportedMappings.some((mapping) => mapping.placeholderKey === 'tel_label') ? 'PASS' : 'FAIL'
|
||||
])
|
||||
supportedMappings.some((mapping) => mapping.placeholderKey === 'tel_label')
|
||||
? 'PASS'
|
||||
: 'FAIL',
|
||||
options.variant === 'product-v1' &&
|
||||
supportedMappings.some((mapping) => mapping.placeholderKey === 'items_table')
|
||||
? 'PASS'
|
||||
: 'PASS',
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
const overallStatus = summarizeIssues(
|
||||
results.map((result) => result.status as 'PASS' | 'WARNING' | 'FAIL')
|
||||
results.map((result) => String(result.status) as 'PASS' | 'WARNING' | 'FAIL'),
|
||||
);
|
||||
|
||||
console.log(
|
||||
@@ -370,12 +431,14 @@ async function main() {
|
||||
{
|
||||
action: 'reseed-pdf-template-version',
|
||||
overallStatus,
|
||||
templateCount: results.length,
|
||||
results
|
||||
variant: options.variant,
|
||||
activate: options.activate,
|
||||
resultCount: results.length,
|
||||
results,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await sql.end({ timeout: 5 });
|
||||
|
||||
Reference in New Issue
Block a user