@open-mercato/core 0.10.1-develop.7994.1.c9f31972aa → 0.10.1-develop.8022.1.b6753de892
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js +80 -50
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +3 -3
- package/dist/modules/audit_logs/lib/rowPresentation.js +82 -0
- package/dist/modules/audit_logs/lib/rowPresentation.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +89 -16
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +7 -0
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +2 -2
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +10 -21
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js +17 -10
- package/dist/modules/customers/migrations/Migration20260825140000_customers.js.map +2 -2
- package/dist/modules/entities/cli.js +138 -66
- package/dist/modules/entities/cli.js.map +3 -3
- package/dist/modules/integrations/lib/state-service.js +1 -1
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/promotions/commands/codes.js +18 -3
- package/dist/modules/promotions/commands/codes.js.map +2 -2
- package/dist/modules/query_index/cli.js +17 -1
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +23 -5
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/resources/commands/resource-types.js +42 -15
- package/dist/modules/resources/commands/resource-types.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +54 -3
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/lib/resourceTypeLock.js +15 -0
- package/dist/modules/resources/lib/resourceTypeLock.js.map +7 -0
- package/dist/modules/sales/commands/configuration.js +30 -16
- package/dist/modules/sales/commands/configuration.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +36 -15
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +35 -6
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +85 -48
- package/src/modules/audit_logs/i18n/de.json +16 -0
- package/src/modules/audit_logs/i18n/en.json +16 -0
- package/src/modules/audit_logs/i18n/es.json +16 -0
- package/src/modules/audit_logs/i18n/ko.json +16 -0
- package/src/modules/audit_logs/i18n/pl.json +16 -0
- package/src/modules/audit_logs/lib/rowPresentation.ts +92 -0
- package/src/modules/catalog/commands/variants.ts +96 -17
- package/src/modules/catalog/seed/examples.ts +7 -0
- package/src/modules/customers/api/interactions/route.ts +2 -2
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +9 -12
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/migrations/Migration20260825140000_customers.ts +21 -10
- package/src/modules/entities/cli.ts +144 -65
- package/src/modules/integrations/lib/state-service.ts +1 -1
- package/src/modules/promotions/commands/codes.ts +19 -4
- package/src/modules/query_index/cli.ts +17 -1
- package/src/modules/query_index/lib/reindexer.ts +55 -6
- package/src/modules/resources/commands/resource-types.ts +47 -15
- package/src/modules/resources/commands/resources.ts +74 -3
- package/src/modules/resources/i18n/de.json +3 -0
- package/src/modules/resources/i18n/en.json +3 -0
- package/src/modules/resources/i18n/es.json +3 -0
- package/src/modules/resources/i18n/ko.json +3 -0
- package/src/modules/resources/i18n/pl.json +3 -0
- package/src/modules/resources/lib/resourceTypeLock.ts +14 -0
- package/src/modules/sales/commands/configuration.ts +35 -16
- package/src/modules/sales/components/documents/AddressesSection.tsx +51 -20
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/ko.json +3 -0
- package/src/modules/sales/i18n/pl.json +4 -1
- package/src/modules/sales/seed/examples.ts +41 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/lib/reindexer.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport {\n upsertIndexBatch,\n assertIndexBatchWritesLanded,\n createEmptyUpsertIndexBatchResult,\n mergeUpsertIndexBatchResults,\n QueryIndexBatchWriteError,\n type AnyRow,\n} from './batch'\nimport {\n refreshCoverageSnapshot,\n writeCoverageCounts,\n applyCoverageAdjustments,\n deleteScopedCoverageForEntity,\n} from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans, retireScopedCatalogueRows } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { isTenantGlobalEntityType } from './tenant-global'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\n/**\n * Why a sweep did no work, when it did none on purpose.\n *\n * A refused run is otherwise byte-identical to a successful sweep that found zero rows,\n * and the surfaces an operator reads - `indexer_status_logs`, the indexer status page,\n * the CLI's `0 / 0 (0.00%)` - report the second. The allowlist's safety argument is that\n * a missing `registerTenantGlobalEntityTypes()` declaration \"shows up in the log\", so the\n * log the product surfaces has to be able to say it.\n */\nexport type ReindexRefusalReason =\n /** The source table has no `tenant_id` column and the type is not a declared catalogue. */\n | 'no-tenant-column'\n /** `information_schema` could not be read, so no column could be proven to exist. */\n | 'column-probe-failed'\n /**\n * The type IS declared tenant-global, but its table carries `organization_id` while\n * lacking `tenant_id`. A global projection means both scope columns absent - the same\n * definition `loadQueryIndexRowScope()` uses - so the declaration cannot be honoured.\n */\n | 'declared-global-but-org-scoped'\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n /** Absent on a sweep that ran. Present, with the reason, on one that deliberately did not. */\n refused?: ReindexRefusalReason\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\n/**\n * Above this many failed records the purge exclusion list stops being a sane query, so\n * the purge is skipped entirely instead. Failing closed keeps stale rows; the alternative\n * deletes index entries the run failed to rebuild.\n */\nconst MAX_PURGE_EXCLUSIONS = 1000\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\n/**\n * Returns `null` - never an empty set - when the probe itself fails.\n *\n * An empty set is indistinguishable from \"this table has no columns we care about\", so a\n * transient failure (an aborted transaction on the same connection, a statement timeout, a\n * blip during a partition-parallel sweep) would make every entity type look tenant-less and\n * refuse every tenant-scoped reindex, reported as a successful zero-row run. Failing closed\n * is right; failing closed silently and globally is the hardest state to diagnose.\n */\nasync function getColumnSet(db: Kysely<any>, tableName: string, schema?: string): Promise<Set<string> | null> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(schema ? sql<boolean>`table_schema = ${schema}` : sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch (error) {\n logger.warn('Failed to read information_schema.columns for a reindex target', {\n table: tableName,\n error: error instanceof Error ? error.message : String(error),\n })\n reportColumnProbeFailure(error, tableName)\n return null\n }\n}\n\n/**\n * The probe's caller refuses the sweep whatever happens here, so a telemetry backend that\n * throws must not turn that refusal into a failed job.\n */\nfunction reportColumnProbeFailure(error: unknown, tableName: string): void {\n try {\n getTelemetryRuntime()?.reportError(error, {\n module: 'query_index',\n code: 'query_index.column_probe_failed',\n attributes: { table: tableName },\n })\n } catch (telemetryError) {\n logger.warn('Failed to report a column probe failure', {\n table: tableName,\n error: telemetryError instanceof Error ? telemetryError.message : String(telemetryError),\n })\n }\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const source = resolveQueryIndexSourceTableMetadata(em, entityType)\n const table = source?.table\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, source?.tableName ?? table, source?.schema)\n // A probe failure proves nothing about the table, so it cannot be read as \"no tenant\n // column\". Refuse with its own reason rather than letting a database blip masquerade as\n // a permanent classification - and refuse rather than proceed, because proceeding would\n // drop the tenant predicate and stamp the caller's tenant, which is the defect this guard\n // exists for.\n if (columns === null) {\n logger.warn(\"Refusing reindex: could not read the source table's columns\", {\n entityType,\n table,\n tenantId,\n })\n return { processed: 0, total: 0, tenantScopes: [], scopes: [], refused: 'column-probe-failed' }\n }\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n // `applyBaseWhere()` below can only apply the tenant predicate when the source\n // table HAS a `tenant_id` column; when it does not, it silently drops the\n // predicate the caller asked for. `scopeOverrides.tenantId` further down then\n // stamps every swept row with the caller's tenant anyway. A tenant-scoped\n // reindex of a tenant-less table therefore does two wrong things at once: it\n // reads every tenant's rows, and it files them under whichever tenant happened\n // to run it. Nothing downstream corrects that, because both readers ask for an\n // exact tenant match with no NULL branch, so the rows become searchable and\n // readable as that tenant's own.\n //\n // The condition is derived, not enumerated: it is exactly the set of inputs\n // where a predicate is dropped AND an override is stamped. A `tenantId` of\n // `undefined` or `null` sets no override, so those rows land under\n // `tenant_id = NULL` \u2014 invisible to both readers, but filed under nobody \u2014 and\n // are deliberately left alone here.\n //\n // Indexing every tenant-less table under NULL was considered as the general fix\n // and rejected: an unqualified NULL branch in the readers overloads a value that\n // also means \"written by an unscoped reindex\", so every mis-scoped row would\n // become globally visible \u2014 fail-open, in a fix whose whole value is failing\n // closed. `isTenantGlobalEntityType` is the narrow allowlist for the catalogue\n // case instead; see its comment in ./tenant-global. The readers widen for exactly\n // that declared set and for nothing else, and the projection of a declared type is\n // written under the null tenant just below.\n // A declared platform-wide catalogue has one row per record for every tenant to\n // share, so its projection belongs to no tenant and no organization in particular and\n // is written under `tenant_id = NULL, organization_id = NULL`. That is already where\n // the incremental path files these rows: `resolveQueryIndexRecordScope()` resolves a\n // source table with NEITHER scope column to `kind: 'global'` and requires an explicitly\n // null tenant AND organization. Before this, the sweep disagreed with it and stamped the\n // caller's scope, and since `entity_indexes` is unique on\n // (entity_type, entity_id, organization_id_coalesced) the last caller to reindex simply\n // took the catalogue from the previous one. Both readers in `@open-mercato/search` now\n // carry the matching NULL branch for declared types.\n //\n // The condition matches `loadQueryIndexRowScope()`'s definition of global exactly -\n // NEITHER column, not just no tenant column. Keying on the tenant column alone would let\n // a third-party declaration of a table that HAS `organization_id` take the global branch\n // for a record the event path never classifies as global, so the two writers would\n // disagree again on a different axis.\n //\n // Only the PROJECTION scope moves. `jobScope` keeps the caller's tenant and organization:\n // it records who ran the sweep, so two tenants reindexing the catalogue remain two jobs\n // rather than one that blocks the other behind the active-job guard.\n const writesGlobalProjection =\n columns.has('id') && !hasTenantCol && !hasOrgCol && !source?.tenantColumn && !source?.organizationColumn && isTenantGlobalEntityType(entityType)\n const projectionTenantId = writesGlobalProjection ? null : tenantId\n const projectionOrganizationId = writesGlobalProjection ? null : organizationId\n // Vector work keeps the caller's scope even where the projection moves. Its consumers -\n // the `query_index.vectorize_one` and `query_index.vectorize_purge` subscribers and\n // `removeOrphans` - need a real tenant, and nothing defines a shared vector across\n // dispatch, storage and reads, so a declared catalogue's vectors stay filed per tenant\n // exactly as they were before its projection moved to the null scope.\n const vectorTenantId = writesGlobalProjection ? tenantId : projectionTenantId\n const vectorOrganizationId = writesGlobalProjection ? organizationId : projectionOrganizationId\n\n // Refuse anything tenant-less that is not writing a global projection. That is the\n // no-tenant-column case, and also a declaration this module cannot honour: a declared\n // type whose table carries `organization_id` but no `tenant_id`. Letting the second\n // through would silently drop the tenant predicate and stamp the caller's tenant - the\n // exact defect the guard exists for - while the author believes the declaration took\n // effect. Fail closed and name it, in the spirit of the allowlist itself.\n if (!hasTenantCol && tenantId != null && !writesGlobalProjection) {\n const declaredButOrgScoped = isTenantGlobalEntityType(entityType)\n logger.warn(\n declaredButOrgScoped\n ? 'Refusing tenant-scoped reindex: entity type is declared tenant-global but its table has an organization_id column'\n : 'Refusing tenant-scoped reindex of a table with no tenant_id column',\n { entityType, table, tenantId },\n )\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n refused: declaredButOrgScoped ? 'declared-global-but-org-scoped' : 'no-tenant-column',\n }\n }\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (projectionTenantId === undefined ? null : projectionTenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (projectionOrganizationId === undefined ? null : projectionOrganizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = projectionTenantId === undefined ? null : projectionTenantId ?? null\n const bucketOrg = projectionOrganizationId === undefined ? null : projectionOrganizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (projectionTenantId !== undefined && projectionTenantId !== null) {\n scopeOverrides.tenantId = String(projectionTenantId)\n }\n if (projectionOrganizationId !== undefined && projectionOrganizationId !== null) {\n scopeOverrides.orgId = String(projectionOrganizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n let jobFailed = false\n const writeTotals = createEmptyUpsertIndexBatchResult()\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n // Partitions run concurrently, so a scope-wide purge here would delete rows a sibling\n // partition has already written and never rebuild them (the run still reports success\n // because progress counts attempted writes). Each partition therefore purges only the\n // slice it is about to rebuild, and every partition purges \u2014 restricting the purge to\n // the coverage-resetting partition would otherwise leave the other slices' stale rows\n // behind on a forced rebuild.\n if (force && (resetCoverage || usingPartitions)) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (projectionTenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${projectionTenantId ?? null}`)\n }\n if (projectionOrganizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${projectionOrganizationId ?? null}`)\n }\n if (usingPartitions && partitionIndex !== null) {\n purgeQuery = purgeQuery.where(\n sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCountRaw}) = ${partitionIndex}`,\n )\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n partitionIndex: usingPartitions ? partitionIndex : null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // The vector purge is scope-wide and cannot be partitioned \u2014 the queued job deletes every\n // vector for the scope. Emitting it from one partition while siblings are already queueing\n // per-record vectorize jobs is the same race the row purge just had, so a partitioned run\n // leaves it to the caller that owns the fan-out (`api/reindex.ts` queues it once, before\n // dispatching any partition).\n if (force && resetCoverage && !usingPartitions && emitVectorize && eventBus) {\n if (vectorTenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: vectorTenantId ?? null,\n }\n if (vectorOrganizationId !== undefined) payload.organizationId = vectorOrganizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n\n if (resetCoverage) {\n // Only meaningful for an unpartitioned run: `baseCounts` holds this partition's slice,\n // so writing it as the scope's base count under-reports by the partition factor, and\n // zeroing indexed_count scope-wide discards deltas siblings have already applied. The\n // authoritative `refreshCoverageSnapshot` at the end of every partition owns the counts\n // for partitioned runs.\n const nowTs = Date.now()\n for (const scope of usingPartitions ? [] : baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n const batchResult = await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n mergeUpsertIndexBatchResults(writeTotals, batchResult)\n\n // A whole batch failing is infrastructural (pool exhausted, disk full, KMS down),\n // not a poison record. Abort now instead of grinding through the rest of the table.\n if (batchResult.written === 0 && batchResult.attempted > 0) {\n throw new QueryIndexBatchWriteError(entityType, writeTotals)\n }\n\n const failedInBatch = new Set(batchResult.failedRecordIds)\n const writtenRows = failedInBatch.size ? rows.filter((row) => !failedInBatch.has(String(row.id))) : rows\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of writtenRows) {\n const scopeTenant = projectionTenantId !== undefined\n ? projectionTenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = projectionOrganizationId !== undefined\n ? projectionOrganizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n writtenRows.map((row) => {\n const scopeOrg = vectorOrganizationId !== undefined\n ? vectorOrganizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = vectorTenantId !== undefined\n ? vectorTenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += batchResult.written\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: batchResult.written })\n await updateJobProgress(db, jobScope, batchResult.written)\n }\n\n // Records this run failed to write still look untouched to the purge predicate, so\n // without the exclusion the purge would delete the index rows it just failed to\n // rebuild \u2014 turning a stale entry into a missing one.\n const purgeExclusions = writeTotals.failedRecordIds\n if (purgeExclusions.length > MAX_PURGE_EXCLUSIONS) {\n logger.warn('Skipping orphan purge after widespread write failures', {\n entityType,\n failedRecords: purgeExclusions.length,\n })\n } else {\n await purgeOrphans(db, {\n entityType,\n tenantId: projectionTenantId,\n organizationId: projectionOrganizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n excludeRecordIds: purgeExclusions,\n })\n // The purge above only sees the canonical null scope. Rows earlier sweeps stamped with\n // a caller's tenant and organization are other keys of the same records, so a sweep of\n // a declared catalogue also retires them - after the canonical rows are written, so the\n // catalogue never goes unserved in between.\n if (writesGlobalProjection) {\n await retireScopedCatalogueRows(db, {\n entityType,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n excludeRecordIds: purgeExclusions,\n })\n if (resetCoverage) await deleteScopedCoverageForEntity(db, entityType)\n }\n }\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId: vectorTenantId,\n organizationId: vectorOrganizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n\n // Deliberately after the coverage refresh: the authoritative recount is what keeps\n // indexed_count truthful, and it is most worth having when a run has just failed.\n // Throwing here fails the queue job so the loss is visible instead of silent.\n if (writeTotals.searchTokenFailures > 0) {\n logger.warn('Search token writes failed during reindex', {\n entityType,\n batches: writeTotals.searchTokenFailures,\n })\n }\n assertIndexBatchWritesLanded(entityType, writeTotals)\n } catch (error) {\n jobFailed = true\n throw error\n } finally {\n // Still finalized on failure: the scope stays wedged behind the active-job guard\n // while finished_at is null. The status carries the outcome instead.\n await finalizeJob(db, jobScope, jobFailed ? { status: 'failed' } : {})\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'\nimport {\n upsertIndexBatch,\n assertIndexBatchWritesLanded,\n createEmptyUpsertIndexBatchResult,\n mergeUpsertIndexBatchResults,\n QueryIndexBatchWriteError,\n type AnyRow,\n} from './batch'\nimport {\n refreshCoverageSnapshot,\n writeCoverageCounts,\n applyCoverageAdjustments,\n deleteScopedCoverageForEntity,\n} from './coverage'\nimport { prepareJob, updateJobProgress, finalizeJob, type JobScope } from './jobs'\nimport { purgeOrphans, retireScopedCatalogueRows } from './stale'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { isSearchDebugEnabled } from './search-tokens'\nimport { isTenantGlobalEntityType } from './tenant-global'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\nconst logger = createLogger('query_index').child({ component: 'reindexer' })\n\nexport type ReindexJobOptions = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n force?: boolean\n batchSize?: number\n emitVectorizeEvents?: boolean\n eventBus?: {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n partitionCount?: number\n partitionIndex?: number\n resetCoverage?: boolean\n onProgress?: (info: { processed: number; total: number; chunkSize: number }) => void\n vectorService?: VectorIndexService | null\n}\n\n/**\n * Why a sweep did no work, when it did none on purpose.\n *\n * A refused run is otherwise byte-identical to a successful sweep that found zero rows,\n * and the surfaces an operator reads - `indexer_status_logs`, the indexer status page,\n * the CLI's `0 / 0 (0.00%)` - report the second. The allowlist's safety argument is that\n * a missing `registerTenantGlobalEntityTypes()` declaration \"shows up in the log\", so the\n * log the product surfaces has to be able to say it.\n */\nexport type ReindexRefusalReason =\n /** The source table has no `tenant_id` column and the type is not a declared catalogue. */\n | 'no-tenant-column'\n /** `information_schema` could not be read, so no column could be proven to exist. */\n | 'column-probe-failed'\n /**\n * The type IS declared tenant-global, but its table carries `organization_id` while\n * lacking `tenant_id`. A global projection means both scope columns absent - the same\n * definition `loadQueryIndexRowScope()` uses - so the declaration cannot be honoured.\n */\n | 'declared-global-but-org-scoped'\n\nexport type ReindexJobResult = {\n processed: number\n total: number\n tenantScopes: Array<string | null>\n scopes: Array<{ tenantId: string | null; organizationId: string | null }>\n /** Absent on a sweep that ran. Present, with the reason, on one that deliberately did not. */\n refused?: ReindexRefusalReason\n}\n\nexport const DEFAULT_REINDEX_PARTITIONS = 5\nconst DEFAULT_BATCH_SIZE = 500\n/**\n * Above this many failed records the purge exclusion list stops being a sane query, so\n * the purge is skipped entirely instead. Failing closed keeps stale rows; the alternative\n * deletes index entries the run failed to rebuild.\n */\nconst MAX_PURGE_EXCLUSIONS = 1000\nconst deriveOrgFromId = new Set<string>(['directory:organization'])\nconst COVERAGE_REFRESH_THROTTLE_MS = 5 * 60 * 1000\nconst lastCoverageReset = new Map<string, number>()\n\nconst REINDEX_DECRYPT_DEBUG_KEYS = ['display_name', 'first_name', 'last_name', 'brand_name', 'legal_name', 'primary_email', 'primary_phone'] as const\n\nexport type ReindexDecryptDebugPayload = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n keys: string[]\n}\n\nexport function buildReindexDecryptDebugPayload(\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n): ReindexDecryptDebugPayload {\n const presentKeys: string[] = []\n for (const key of REINDEX_DECRYPT_DEBUG_KEYS) {\n const value = doc[key]\n if (key in doc && value != null && value !== '') presentKeys.push(key)\n }\n return {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n keys: presentKeys,\n }\n}\n\nasync function cleanupLegacyJobScopes(\n db: Kysely<any>,\n options: {\n entityType: string\n organizationId: string | null\n tenantId: string | null\n activePartitionCount: number | null\n },\n): Promise<void> {\n await db\n .deleteFrom('entity_index_jobs' as any)\n .where('entity_type' as any, '=', options.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${options.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${options.tenantId}`)\n .where(sql<boolean>`partition_count is distinct from ${options.activePartitionCount}`)\n .execute()\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\n/**\n * Returns `null` - never an empty set - when the probe itself fails.\n *\n * An empty set is indistinguishable from \"this table has no columns we care about\", so a\n * transient failure (an aborted transaction on the same connection, a statement timeout, a\n * blip during a partition-parallel sweep) would make every entity type look tenant-less and\n * refuse every tenant-scoped reindex, reported as a successful zero-row run. Failing closed\n * is right; failing closed silently and globally is the hardest state to diagnose.\n */\nasync function getColumnSet(db: Kysely<any>, tableName: string, schema?: string): Promise<Set<string> | null> {\n try {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['column_name' as any])\n .where(schema ? sql<boolean>`table_schema = ${schema}` : sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', tableName)\n .execute() as Array<{ column_name: string }>\n return new Set(rows.map((row) => String(row.column_name).toLowerCase()))\n } catch (error) {\n logger.warn('Failed to read information_schema.columns for a reindex target', {\n table: tableName,\n error: error instanceof Error ? error.message : String(error),\n })\n reportColumnProbeFailure(error, tableName)\n return null\n }\n}\n\n/**\n * The probe's caller refuses the sweep whatever happens here, so a telemetry backend that\n * throws must not turn that refusal into a failed job.\n */\nfunction reportColumnProbeFailure(error: unknown, tableName: string): void {\n try {\n getTelemetryRuntime()?.reportError(error, {\n module: 'query_index',\n code: 'query_index.column_probe_failed',\n attributes: { table: tableName },\n })\n } catch (telemetryError) {\n logger.warn('Failed to report a column probe failure', {\n table: tableName,\n error: telemetryError instanceof Error ? telemetryError.message : String(telemetryError),\n })\n }\n}\n\ntype ReindexSourceMetadata = ReturnType<typeof resolveQueryIndexSourceTableMetadata>\n\nfunction writesGlobalReindexProjection(\n entityType: string,\n columns: Set<string>,\n source: ReindexSourceMetadata,\n): boolean {\n return columns.has('id')\n && !columns.has('tenant_id')\n && !columns.has('organization_id')\n && !source?.tenantColumn\n && !source?.organizationColumn\n && isTenantGlobalEntityType(entityType)\n}\n\nfunction resolveTenantlessRefusal(\n entityType: string,\n columns: Set<string>,\n source: ReindexSourceMetadata,\n): Exclude<ReindexRefusalReason, 'column-probe-failed'> | null {\n if (columns.has('tenant_id')) return null\n if (writesGlobalReindexProjection(entityType, columns, source)) return null\n return isTenantGlobalEntityType(entityType) ? 'declared-global-but-org-scoped' : 'no-tenant-column'\n}\n\n/**\n * Answers, without logging, whether a tenant-scoped sweep of `entityType` would be refused\n * because its source table has no `tenant_id` column and the type is not a declared\n * tenant-global catalogue. Callers that enumerate every registered entity type for one\n * tenant (e.g. `mercato query_index reindex --tenant <id>` during `mercato init`) use it to\n * leave such types out up front, so the guard inside `reindexEntity()` keeps warning only\n * for callers that genuinely aimed a tenant-scoped sweep at a tenant-less table.\n *\n * Deliberately narrow: an unregistered type, a failed column probe, or a tenant-global\n * declaration that cannot be honoured all return `false`, so those still reach the guard\n * and surface their own diagnostics.\n */\nexport async function isTenantlessReindexTarget(\n em: EntityManager,\n entityType: string,\n): Promise<boolean> {\n if (!entityType) return false\n const source = resolveQueryIndexSourceTableMetadata(em, entityType)\n if (!source?.table) return false\n const db = em.getKysely<Record<string, Record<string, unknown>>>()\n const columns = await getColumnSet(db, source.tableName ?? source.table, source.schema)\n if (columns === null) return false\n return resolveTenantlessRefusal(entityType, columns, source) === 'no-tenant-column'\n}\n\nexport async function reindexEntity(\n em: EntityManager,\n options: ReindexJobOptions,\n): Promise<ReindexJobResult> {\n const entityType = String(options?.entityType || '')\n if (!entityType) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const tenantIdInput = options?.tenantId\n const tenantId = tenantIdInput === 'undefined' ? undefined : tenantIdInput\n const organizationIdInput = options?.organizationId\n const organizationId = organizationIdInput === 'undefined' ? undefined : organizationIdInput\n const force = options?.force === true\n const batchSize = Number.isFinite(options?.batchSize) && options!.batchSize! > 0\n ? Math.max(1, Math.trunc(options!.batchSize!))\n : DEFAULT_BATCH_SIZE\n const emitVectorize = options?.emitVectorizeEvents === true\n const eventBus = options?.eventBus\n const vectorService = options?.vectorService ?? null\n const partitionCountRaw = Number.isFinite(options?.partitionCount)\n ? Math.max(1, Math.trunc(options!.partitionCount!))\n : 1\n const usingPartitions = partitionCountRaw > 1\n const partitionIndexRaw = Number.isFinite(options?.partitionIndex)\n ? Math.max(0, Math.trunc(options!.partitionIndex!))\n : 0\n const partitionIndex = usingPartitions\n ? Math.min(partitionIndexRaw, partitionCountRaw - 1)\n : null\n const resetCoverage = options?.resetCoverage ?? (!usingPartitions || partitionIndex === 0)\n\n const db = (em as any).getKysely() as Kysely<any>\n // Resolve the source table strictly via registered MikroORM metadata. We must\n // never fall back to a pluralized guess derived from the caller-supplied id\n // here: doing so would let a principal with `query_index.reindex` point the\n // reindexer at arbitrary tables (e.g. `auth_users`, `users`) and read their\n // rows into the index, bypassing tenant scoping and entity-level encryption.\n const source = resolveQueryIndexSourceTableMetadata(em, entityType)\n const table = source?.table\n if (!table || entityType === 'query_index:search_token' || table === 'search_tokens') {\n if (!table) {\n logger.warn('Refusing to reindex unregistered entity type', {\n entityType,\n })\n }\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n const columns = await getColumnSet(db, source?.tableName ?? table, source?.schema)\n // A probe failure proves nothing about the table, so it cannot be read as \"no tenant\n // column\". Refuse with its own reason rather than letting a database blip masquerade as\n // a permanent classification - and refuse rather than proceed, because proceeding would\n // drop the tenant predicate and stamp the caller's tenant, which is the defect this guard\n // exists for.\n if (columns === null) {\n logger.warn(\"Refusing reindex: could not read the source table's columns\", {\n entityType,\n table,\n tenantId,\n })\n return { processed: 0, total: 0, tenantScopes: [], scopes: [], refused: 'column-probe-failed' }\n }\n const hasOrgCol = columns.has('organization_id')\n const hasTenantCol = columns.has('tenant_id')\n const hasDeletedCol = columns.has('deleted_at')\n\n // `applyBaseWhere()` below can only apply the tenant predicate when the source\n // table HAS a `tenant_id` column; when it does not, it silently drops the\n // predicate the caller asked for. `scopeOverrides.tenantId` further down then\n // stamps every swept row with the caller's tenant anyway. A tenant-scoped\n // reindex of a tenant-less table therefore does two wrong things at once: it\n // reads every tenant's rows, and it files them under whichever tenant happened\n // to run it. Nothing downstream corrects that, because both readers ask for an\n // exact tenant match with no NULL branch, so the rows become searchable and\n // readable as that tenant's own.\n //\n // The condition is derived, not enumerated: it is exactly the set of inputs\n // where a predicate is dropped AND an override is stamped. A `tenantId` of\n // `undefined` or `null` sets no override, so those rows land under\n // `tenant_id = NULL` \u2014 invisible to both readers, but filed under nobody \u2014 and\n // are deliberately left alone here.\n //\n // Indexing every tenant-less table under NULL was considered as the general fix\n // and rejected: an unqualified NULL branch in the readers overloads a value that\n // also means \"written by an unscoped reindex\", so every mis-scoped row would\n // become globally visible \u2014 fail-open, in a fix whose whole value is failing\n // closed. `isTenantGlobalEntityType` is the narrow allowlist for the catalogue\n // case instead; see its comment in ./tenant-global. The readers widen for exactly\n // that declared set and for nothing else, and the projection of a declared type is\n // written under the null tenant just below.\n // A declared platform-wide catalogue has one row per record for every tenant to\n // share, so its projection belongs to no tenant and no organization in particular and\n // is written under `tenant_id = NULL, organization_id = NULL`. That is already where\n // the incremental path files these rows: `resolveQueryIndexRecordScope()` resolves a\n // source table with NEITHER scope column to `kind: 'global'` and requires an explicitly\n // null tenant AND organization. Before this, the sweep disagreed with it and stamped the\n // caller's scope, and since `entity_indexes` is unique on\n // (entity_type, entity_id, organization_id_coalesced) the last caller to reindex simply\n // took the catalogue from the previous one. Both readers in `@open-mercato/search` now\n // carry the matching NULL branch for declared types.\n //\n // The condition matches `loadQueryIndexRowScope()`'s definition of global exactly -\n // NEITHER column, not just no tenant column. Keying on the tenant column alone would let\n // a third-party declaration of a table that HAS `organization_id` take the global branch\n // for a record the event path never classifies as global, so the two writers would\n // disagree again on a different axis.\n //\n // Only the PROJECTION scope moves. `jobScope` keeps the caller's tenant and organization:\n // it records who ran the sweep, so two tenants reindexing the catalogue remain two jobs\n // rather than one that blocks the other behind the active-job guard.\n const writesGlobalProjection = writesGlobalReindexProjection(entityType, columns, source)\n const projectionTenantId = writesGlobalProjection ? null : tenantId\n const projectionOrganizationId = writesGlobalProjection ? null : organizationId\n // Vector work keeps the caller's scope even where the projection moves. Its consumers -\n // the `query_index.vectorize_one` and `query_index.vectorize_purge` subscribers and\n // `removeOrphans` - need a real tenant, and nothing defines a shared vector across\n // dispatch, storage and reads, so a declared catalogue's vectors stay filed per tenant\n // exactly as they were before its projection moved to the null scope.\n const vectorTenantId = writesGlobalProjection ? tenantId : projectionTenantId\n const vectorOrganizationId = writesGlobalProjection ? organizationId : projectionOrganizationId\n\n // Refuse anything tenant-less that is not writing a global projection. That is the\n // no-tenant-column case, and also a declaration this module cannot honour: a declared\n // type whose table carries `organization_id` but no `tenant_id`. Letting the second\n // through would silently drop the tenant predicate and stamp the caller's tenant - the\n // exact defect the guard exists for - while the author believes the declaration took\n // effect. Fail closed and name it, in the spirit of the allowlist itself.\n const tenantlessRefusal = tenantId != null ? resolveTenantlessRefusal(entityType, columns, source) : null\n if (tenantlessRefusal) {\n logger.warn(\n tenantlessRefusal === 'declared-global-but-org-scoped'\n ? 'Refusing tenant-scoped reindex: entity type is declared tenant-global but its table has an organization_id column'\n : 'Refusing tenant-scoped reindex of a table with no tenant_id column',\n { entityType, table, tenantId },\n )\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n refused: tenantlessRefusal,\n }\n }\n\n const jobScope: JobScope = {\n entityType,\n organizationId: organizationId ?? null,\n tenantId: tenantId ?? null,\n partitionIndex,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n }\n\n if (!force) {\n const activeJob = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['id' as any])\n .where('entity_type' as any, '=', entityType)\n .where('finished_at' as any, 'is', null as any)\n .where(sql<boolean>`organization_id is not distinct from ${null}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .executeTakeFirst()\n if (activeJob) {\n return {\n processed: 0,\n total: 0,\n tenantScopes: [],\n scopes: [],\n }\n }\n }\n\n if (resetCoverage) {\n await cleanupLegacyJobScopes(db, {\n entityType,\n organizationId: jobScope.organizationId ?? null,\n tenantId: jobScope.tenantId ?? null,\n activePartitionCount: jobScope.partitionCount ?? null,\n })\n }\n\n const scopeKey = (tenantValue: string | null, orgValue: string | null) => `${tenantValue ?? '__null__'}|${orgValue ?? '__null__'}`\n\n const applyBaseWhere = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n if (hasDeletedCol) chain = chain.where('b.deleted_at' as any, 'is', null as any)\n if (tenantId !== undefined && hasTenantCol) {\n chain = tenantId === null\n ? chain.where('b.tenant_id' as any, 'is', null as any)\n : chain.where('b.tenant_id' as any, '=', tenantId)\n }\n if (organizationId !== undefined && hasOrgCol) {\n chain = organizationId === null\n ? chain.where('b.organization_id' as any, 'is', null as any)\n : chain.where('b.organization_id' as any, '=', organizationId)\n }\n if (usingPartitions && partitionIndex !== null) {\n chain = chain.where(sql<boolean>`mod(abs(hashtext(b.id::text)), ${partitionCountRaw}) = ${partitionIndex}`)\n }\n return chain\n }\n\n type ScopeStats = { tenantId: string | null; organizationId: string | null; count: number }\n const baseCounts = new Map<string, ScopeStats>()\n const registerBaseCount = (tenantValue: string | null, orgValue: string | null, count: number) => {\n const key = scopeKey(tenantValue, orgValue)\n baseCounts.set(key, { tenantId: tenantValue, organizationId: orgValue, count })\n }\n\n const groupByTenant = hasTenantCol && tenantId === undefined\n const groupByOrg = hasOrgCol && organizationId === undefined\n\n if (groupByTenant || groupByOrg) {\n let groupQuery = applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n )\n if (groupByTenant) {\n groupQuery = groupQuery.select('b.tenant_id as tenant_id' as any).groupBy('b.tenant_id' as any)\n }\n if (groupByOrg) {\n groupQuery = groupQuery.select('b.organization_id as organization_id' as any).groupBy('b.organization_id' as any)\n }\n const rows = await groupQuery.execute() as Array<Record<string, unknown>>\n for (const row of rows) {\n const bucketTenant = groupByTenant\n ? ((row as any)?.tenant_id ?? null)\n : (projectionTenantId === undefined ? null : projectionTenantId ?? null)\n const bucketOrg = groupByOrg\n ? ((row as any)?.organization_id ?? null)\n : (projectionOrganizationId === undefined ? null : projectionOrganizationId ?? null)\n registerBaseCount(bucketTenant, bucketOrg, toNumber((row as any)?.count))\n }\n } else {\n const row = await applyBaseWhere(\n db.selectFrom(`${table} as b` as any).select(sql<number>`count(*)`.as('count')),\n ).executeTakeFirst() as { count: unknown } | undefined\n const bucketTenant = projectionTenantId === undefined ? null : projectionTenantId ?? null\n const bucketOrg = projectionOrganizationId === undefined ? null : projectionOrganizationId ?? null\n registerBaseCount(bucketTenant, bucketOrg, toNumber(row?.count))\n }\n\n const total = Array.from(baseCounts.values()).reduce((acc, value) => acc + (Number.isFinite(value.count) ? value.count : 0), 0)\n await prepareJob(db, jobScope, 'reindexing', { totalCount: total })\n const jobRow = await db\n .selectFrom('entity_index_jobs' as any)\n .select(['started_at' as any])\n .where('entity_type' as any, '=', entityType)\n .where('organization_id' as any, 'is', null as any)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId ?? null}`)\n .where(sql<boolean>`partition_index is not distinct from ${partitionIndex}`)\n .where(sql<boolean>`partition_count is not distinct from ${usingPartitions ? partitionCountRaw : null}`)\n .orderBy('started_at' as any, 'desc')\n .executeTakeFirst() as { started_at: Date | string } | undefined\n const jobStartedAt = jobRow?.started_at ? new Date(jobRow.started_at) : new Date()\n const deriveOrg = deriveOrgFromId.has(entityType)\n ? (row: AnyRow) => String(row.id)\n : undefined\n\n const scopeOverrides: { tenantId?: string; orgId?: string } = {}\n if (projectionTenantId !== undefined && projectionTenantId !== null) {\n scopeOverrides.tenantId = String(projectionTenantId)\n }\n if (projectionOrganizationId !== undefined && projectionOrganizationId !== null) {\n scopeOverrides.orgId = String(projectionOrganizationId)\n }\n\n const scopeEntries = Array.from(baseCounts.values()).map((entry) => ({\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n }))\n const tenantScopes = Array.from(\n new Set(scopeEntries.map((entry) => entry.tenantId ?? null)),\n )\n\n let processed = 0\n let lastId: string | null = null\n let jobFailed = false\n const writeTotals = createEmptyUpsertIndexBatchResult()\n\n options?.onProgress?.({ processed, total, chunkSize: 0 })\n\n // Partitions run concurrently, so a scope-wide purge here would delete rows a sibling\n // partition has already written and never rebuild them (the run still reports success\n // because progress counts attempted writes). Each partition therefore purges only the\n // slice it is about to rebuild, and every partition purges \u2014 restricting the purge to\n // the coverage-resetting partition would otherwise leave the other slices' stale rows\n // behind on a forced rebuild.\n if (force && (resetCoverage || usingPartitions)) {\n try {\n let purgeQuery = db\n .deleteFrom('entity_indexes' as any)\n .where('entity_type' as any, '=', entityType)\n if (projectionTenantId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`tenant_id is not distinct from ${projectionTenantId ?? null}`)\n }\n if (projectionOrganizationId !== undefined) {\n purgeQuery = purgeQuery.where(sql<boolean>`organization_id is not distinct from ${projectionOrganizationId ?? null}`)\n }\n if (usingPartitions && partitionIndex !== null) {\n purgeQuery = purgeQuery.where(\n sql<boolean>`mod(abs(hashtext(entity_id::text)), ${partitionCountRaw}) = ${partitionIndex}`,\n )\n }\n await purgeQuery.execute()\n } catch (error) {\n logger.warn('Failed to purge index rows before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n partitionIndex: usingPartitions ? partitionIndex : null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // The vector purge is scope-wide and cannot be partitioned \u2014 the queued job deletes every\n // vector for the scope. Emitting it from one partition while siblings are already queueing\n // per-record vectorize jobs is the same race the row purge just had, so a partitioned run\n // leaves it to the caller that owns the fan-out (`api/reindex.ts` queues it once, before\n // dispatching any partition).\n if (force && resetCoverage && !usingPartitions && emitVectorize && eventBus) {\n if (vectorTenantId !== undefined) {\n const payload: Record<string, unknown> = {\n entityType,\n tenantId: vectorTenantId ?? null,\n }\n if (vectorOrganizationId !== undefined) payload.organizationId = vectorOrganizationId ?? null\n try {\n await eventBus.emitEvent('query_index.vectorize_purge', payload)\n } catch (err) {\n logger.warn('Failed to queue vector purge before force reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: err instanceof Error ? err.message : err,\n })\n }\n } else {\n logger.warn('Skipping vector purge for force reindex without tenant scope', {\n entityType,\n })\n }\n }\n\n if (resetCoverage) {\n // Only meaningful for an unpartitioned run: `baseCounts` holds this partition's slice,\n // so writing it as the scope's base count under-reports by the partition factor, and\n // zeroing indexed_count scope-wide discards deltas siblings have already applied. The\n // authoritative `refreshCoverageSnapshot` at the end of every partition owns the counts\n // for partitioned runs.\n const nowTs = Date.now()\n for (const scope of usingPartitions ? [] : baseCounts.values()) {\n const key = `${entityType}|${scopeKey(scope.tenantId, scope.organizationId)}`\n const last = lastCoverageReset.get(key) ?? 0\n if (force || nowTs - last >= COVERAGE_REFRESH_THROTTLE_MS) {\n await writeCoverageCounts(em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n }, {\n baseCount: scope.count,\n indexedCount: 0,\n vectorCount: emitVectorize ? 0 : undefined,\n })\n lastCoverageReset.set(key, nowTs)\n }\n }\n }\n\n try {\n while (true) {\n let query = applyBaseWhere(\n db\n .selectFrom(`${table} as b` as any)\n .selectAll('b' as any)\n .orderBy('b.id' as any, 'asc')\n .limit(batchSize),\n )\n if (lastId !== null) {\n query = query.where('b.id' as any, '>', lastId)\n }\n const rows = await query.execute() as AnyRow[]\n if (!rows.length) break\n\n const encryption = resolveTenantEncryptionService(em as any)\n const dekKeyCache = new Map<string | null, string | null>()\n const encryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n return await encryptIndexDocForStorage(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n )\n }\n const decryptDoc = async (\n targetEntity: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => {\n const result = await decryptIndexDocForSearch(\n targetEntity,\n doc,\n { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },\n encryption,\n dekKeyCache,\n )\n if (isSearchDebugEnabled()) {\n logger.debug('Reindex decrypt', buildReindexDecryptDebugPayload(targetEntity, result as Record<string, unknown>, scope))\n }\n return result\n }\n\n const batchResult = await upsertIndexBatch(db, entityType, rows, scopeOverrides, { deriveOrganizationId: deriveOrg, encryptDoc, decryptDoc })\n mergeUpsertIndexBatchResults(writeTotals, batchResult)\n\n // A whole batch failing is infrastructural (pool exhausted, disk full, KMS down),\n // not a poison record. Abort now instead of grinding through the rest of the table.\n if (batchResult.written === 0 && batchResult.attempted > 0) {\n throw new QueryIndexBatchWriteError(entityType, writeTotals)\n }\n\n const failedInBatch = new Set(batchResult.failedRecordIds)\n const writtenRows = failedInBatch.size ? rows.filter((row) => !failedInBatch.has(String(row.id))) : rows\n\n const coverageDeltas = new Map<string, { tenantId: string | null; organizationId: string | null; delta: number }>()\n for (const row of writtenRows) {\n const scopeTenant = projectionTenantId !== undefined\n ? projectionTenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n const scopeOrg = projectionOrganizationId !== undefined\n ? projectionOrganizationId ?? null\n : (hasOrgCol ? ((row as AnyRow).organization_id ?? null) : (deriveOrg ? deriveOrg(row) ?? null : null))\n const key = scopeKey(scopeTenant ?? null, scopeOrg ?? null)\n const existingDelta = coverageDeltas.get(key)\n if (existingDelta) existingDelta.delta += 1\n else coverageDeltas.set(key, {\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n delta: 1,\n })\n }\n if (coverageDeltas.size > 0) {\n await applyCoverageAdjustments(\n em,\n Array.from(coverageDeltas.values()).map((entry) => ({\n entityType,\n tenantId: entry.tenantId,\n organizationId: entry.organizationId,\n withDeleted: false,\n deltaBase: 0,\n deltaIndex: entry.delta,\n })),\n )\n }\n\n if (emitVectorize && eventBus) {\n await Promise.all(\n writtenRows.map((row) => {\n const scopeOrg = vectorOrganizationId !== undefined\n ? vectorOrganizationId ?? null\n : hasOrgCol\n ? ((row as AnyRow).organization_id ?? null)\n : (deriveOrg ? deriveOrg(row) ?? null : null)\n const scopeTenant = vectorTenantId !== undefined\n ? vectorTenantId ?? null\n : (hasTenantCol ? ((row as AnyRow).tenant_id ?? null) : null)\n return eventBus\n .emitEvent('query_index.vectorize_one', {\n entityType,\n recordId: String(row.id),\n organizationId: scopeOrg,\n tenantId: scopeTenant,\n })\n .catch(() => undefined)\n }),\n )\n }\n\n processed += batchResult.written\n lastId = String(rows[rows.length - 1]!.id)\n options?.onProgress?.({ processed, total, chunkSize: batchResult.written })\n await updateJobProgress(db, jobScope, batchResult.written)\n }\n\n // Records this run failed to write still look untouched to the purge predicate, so\n // without the exclusion the purge would delete the index rows it just failed to\n // rebuild \u2014 turning a stale entry into a missing one.\n const purgeExclusions = writeTotals.failedRecordIds\n if (purgeExclusions.length > MAX_PURGE_EXCLUSIONS) {\n logger.warn('Skipping orphan purge after widespread write failures', {\n entityType,\n failedRecords: purgeExclusions.length,\n })\n } else {\n await purgeOrphans(db, {\n entityType,\n tenantId: projectionTenantId,\n organizationId: projectionOrganizationId,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n startedAt: jobStartedAt,\n excludeRecordIds: purgeExclusions,\n })\n // The purge above only sees the canonical null scope. Rows earlier sweeps stamped with\n // a caller's tenant and organization are other keys of the same records, so a sweep of\n // a declared catalogue also retires them - after the canonical rows are written, so the\n // catalogue never goes unserved in between.\n if (writesGlobalProjection) {\n await retireScopedCatalogueRows(db, {\n entityType,\n partitionIndex: usingPartitions ? partitionIndex : null,\n partitionCount: usingPartitions ? partitionCountRaw : null,\n excludeRecordIds: purgeExclusions,\n })\n if (resetCoverage) await deleteScopedCoverageForEntity(db, entityType)\n }\n }\n\n if (force && vectorService && (!usingPartitions || partitionIndex === null)) {\n try {\n await vectorService.removeOrphans({\n entityId: entityType,\n tenantId: vectorTenantId,\n organizationId: vectorOrganizationId,\n olderThan: jobStartedAt,\n })\n } catch (error) {\n logger.warn('Failed to prune vector orphans after reindex', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n for (const scope of scopeEntries) {\n await refreshCoverageSnapshot(\n em,\n {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n withDeleted: false,\n },\n )\n }\n\n // Deliberately after the coverage refresh: the authoritative recount is what keeps\n // indexed_count truthful, and it is most worth having when a run has just failed.\n // Throwing here fails the queue job so the loss is visible instead of silent.\n if (writeTotals.searchTokenFailures > 0) {\n logger.warn('Search token writes failed during reindex', {\n entityType,\n batches: writeTotals.searchTokenFailures,\n })\n }\n assertIndexBatchWritesLanded(entityType, writeTotals)\n } catch (error) {\n jobFailed = true\n throw error\n } finally {\n // Still finalized on failure: the scope stays wedged behind the active-job guard\n // while finished_at is null. The status carries the outcome instead.\n await finalizeJob(db, jobScope, jobFailed ? { status: 'failed' } : {})\n }\n\n return {\n processed,\n total,\n scopes: scopeEntries,\n tenantScopes,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,12 +4,14 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
|
|
|
4
4
|
import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, parseWithCustomFields, setCustomFieldsIfAny } from "@open-mercato/shared/lib/commands/helpers";
|
|
5
5
|
import { buildCustomFieldResetMap, diffCustomFieldChanges, loadCustomFieldSnapshot } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
|
|
6
6
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
7
8
|
import { ResourcesResource, ResourcesResourceType } from "../data/entities.js";
|
|
8
9
|
import {
|
|
9
10
|
resourcesResourceTypeCreateSchema,
|
|
10
11
|
resourcesResourceTypeUpdateSchema
|
|
11
12
|
} from "../data/validators.js";
|
|
12
13
|
import { resourcesResourceTypeCrudEvents } from "../lib/crud.js";
|
|
14
|
+
import { lockResourceType } from "../lib/resourceTypeLock.js";
|
|
13
15
|
import { makeCreateRedo } from "@open-mercato/shared/lib/commands/redo";
|
|
14
16
|
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from "./shared.js";
|
|
15
17
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
@@ -44,6 +46,20 @@ async function loadResourceTypeCustomSnapshot(em, snapshot) {
|
|
|
44
46
|
organizationId: snapshot.organizationId
|
|
45
47
|
});
|
|
46
48
|
}
|
|
49
|
+
async function assertNoAssignedResources(em, resourceType, status) {
|
|
50
|
+
const assignedCount = await em.count(ResourcesResource, {
|
|
51
|
+
resourceTypeId: resourceType.id,
|
|
52
|
+
deletedAt: null,
|
|
53
|
+
tenantId: resourceType.tenantId,
|
|
54
|
+
organizationId: resourceType.organizationId
|
|
55
|
+
});
|
|
56
|
+
if (assignedCount === 0) return;
|
|
57
|
+
const { translate } = await resolveTranslations();
|
|
58
|
+
throw new CrudHttpError(status, {
|
|
59
|
+
error: translate("resources.resourceTypes.errors.deleteAssigned", "Resource type has assigned resources."),
|
|
60
|
+
code: "resources.resourceTypes.errors.deleteAssigned"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
47
63
|
const createResourceTypeCommand = {
|
|
48
64
|
id: "resources.resourceTypes.create",
|
|
49
65
|
async execute(rawInput, ctx) {
|
|
@@ -122,9 +138,24 @@ const createResourceTypeCommand = {
|
|
|
122
138
|
const em = ctx.container.resolve("em").fork();
|
|
123
139
|
const resourceType = await em.findOne(ResourcesResourceType, { id: after.id });
|
|
124
140
|
if (resourceType) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
let deleted = false;
|
|
142
|
+
await withAtomicFlush(em, [
|
|
143
|
+
async () => {
|
|
144
|
+
await lockResourceType(em, resourceType.id);
|
|
145
|
+
const isLive = await em.count(ResourcesResourceType, {
|
|
146
|
+
id: resourceType.id,
|
|
147
|
+
tenantId: resourceType.tenantId,
|
|
148
|
+
organizationId: resourceType.organizationId,
|
|
149
|
+
deletedAt: null
|
|
150
|
+
}) > 0;
|
|
151
|
+
if (!isLive) return;
|
|
152
|
+
await assertNoAssignedResources(em, resourceType, 409);
|
|
153
|
+
resourceType.deletedAt = /* @__PURE__ */ new Date();
|
|
154
|
+
resourceType.updatedAt = /* @__PURE__ */ new Date();
|
|
155
|
+
deleted = true;
|
|
156
|
+
}
|
|
157
|
+
], { transaction: true });
|
|
158
|
+
if (!deleted) return;
|
|
128
159
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
129
160
|
await emitCrudUndoSideEffects({
|
|
130
161
|
dataEngine,
|
|
@@ -323,18 +354,14 @@ const deleteResourceTypeCommand = {
|
|
|
323
354
|
if (!record) throw new CrudHttpError(404, { error: "Resources resource type not found." });
|
|
324
355
|
ensureTenantScope(ctx, record.tenantId);
|
|
325
356
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
record.deletedAt = /* @__PURE__ */ new Date();
|
|
336
|
-
record.updatedAt = /* @__PURE__ */ new Date();
|
|
337
|
-
await em.flush();
|
|
357
|
+
await withAtomicFlush(em, [
|
|
358
|
+
async () => {
|
|
359
|
+
await lockResourceType(em, record.id);
|
|
360
|
+
await assertNoAssignedResources(em, record, 400);
|
|
361
|
+
record.deletedAt = /* @__PURE__ */ new Date();
|
|
362
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
363
|
+
}
|
|
364
|
+
], { transaction: true });
|
|
338
365
|
const dataEngine = ctx.container.resolve("dataEngine");
|
|
339
366
|
await emitCrudSideEffects({
|
|
340
367
|
dataEngine,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/resources/commands/resource-types.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, parseWithCustomFields, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { buildCustomFieldResetMap, diffCustomFieldChanges, loadCustomFieldSnapshot, type CustomFieldSnapshot } from '@open-mercato/shared/lib/commands/customFieldSnapshots'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { ResourcesResource, ResourcesResourceType } from '../data/entities'\nimport {\n resourcesResourceTypeCreateSchema,\n resourcesResourceTypeUpdateSchema,\n type ResourcesResourceTypeCreateInput,\n type ResourcesResourceTypeUpdateInput,\n} from '../data/validators'\nimport { resourcesResourceTypeCrudEvents } from '../lib/crud'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { E } from '#generated/entities.ids.generated'\n\nconst resourceTypeCrudIndexer: CrudIndexerConfig<ResourcesResourceType> = {\n entityType: E.resources.resources_resource_type,\n}\n\ntype ResourceTypeSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n name: string\n description: string | null\n appearanceIcon: string | null\n appearanceColor: string | null\n deletedAt: string | null\n}\n\ntype ResourceTypeUndoPayload = {\n before?: ResourceTypeSnapshot | null\n after?: ResourceTypeSnapshot | null\n customBefore?: CustomFieldSnapshot | null\n customAfter?: CustomFieldSnapshot | null\n}\n\nasync function loadResourceTypeSnapshot(em: EntityManager, id: string): Promise<ResourceTypeSnapshot | null> {\n const resourceType = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id },\n undefined,\n { tenantId: null, organizationId: null },\n )\n if (!resourceType) return null\n return {\n id: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n name: resourceType.name,\n description: resourceType.description ?? null,\n appearanceIcon: resourceType.appearanceIcon ?? null,\n appearanceColor: resourceType.appearanceColor ?? null,\n deletedAt: resourceType.deletedAt ? resourceType.deletedAt.toISOString() : null,\n }\n}\n\nasync function loadResourceTypeCustomSnapshot(em: EntityManager, snapshot: ResourceTypeSnapshot): Promise<CustomFieldSnapshot> {\n return loadCustomFieldSnapshot(em, {\n entityId: E.resources.resources_resource_type,\n recordId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n })\n}\n\nconst createResourceTypeCommand: CommandHandler<ResourcesResourceTypeCreateInput, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.create',\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(resourcesResourceTypeCreateSchema, rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n const record = em.create(ResourcesResourceType, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n name: parsed.name,\n description: parsed.description ?? null,\n appearanceIcon: parsed.appearanceIcon ?? null,\n appearanceColor: parsed.appearanceColor ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n await em.flush()\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadResourceTypeSnapshot(em, result.resourceTypeId)\n if (!snapshot) return null\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { snapshot, custom }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadResourceTypeSnapshot(em, result.resourceTypeId)\n if (!snapshot) return null\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.create', 'Create resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n customAfter: custom,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const resourceType = await em.findOne(ResourcesResourceType, { id: after.id })\n if (resourceType) {\n resourceType.deletedAt = new Date()\n resourceType.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'deleted',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n }\n },\n redo: makeCreateRedo<ResourcesResourceType, ResourceTypeSnapshot, ResourcesResourceTypeCreateInput, { resourceTypeId: string }>({\n entityClass: ResourcesResourceType,\n buildResult: (entity) => ({ resourceTypeId: entity.id }),\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n afterRestore: async ({ ctx, entity, logEntry }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n if (!payload?.customAfter) return\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n const reset = buildCustomFieldResetMap(payload.customAfter, undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n values: reset,\n })\n },\n }),\n}\n\nconst updateResourceTypeCommand: CommandHandler<ResourcesResourceTypeUpdateInput, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.update',\n async prepare(rawInput, ctx) {\n const { parsed } = parseWithCustomFields(resourcesResourceTypeUpdateSchema, rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadResourceTypeSnapshot(em, parsed.id)\n if (!snapshot) return {}\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { before: snapshot, customBefore: custom }\n },\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(resourcesResourceTypeUpdateSchema, rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id: parsed.id, deletedAt: null },\n undefined,\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n if (!record) throw new CrudHttpError(404, { error: 'Resources resource type not found.' })\n ensureTenantScope(ctx, record.tenantId)\n ensureOrganizationScope(ctx, record.organizationId)\n\n if (parsed.name !== undefined) record.name = parsed.name\n if (parsed.description !== undefined) record.description = parsed.description ?? null\n if (parsed.appearanceIcon !== undefined) record.appearanceIcon = parsed.appearanceIcon ?? null\n if (parsed.appearanceColor !== undefined) record.appearanceColor = parsed.appearanceColor ?? null\n record.updatedAt = new Date()\n\n await em.flush()\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = snapshots.before as ResourceTypeSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadResourceTypeSnapshot(em, before.id)\n if (!after) return null\n const customBefore = (snapshots as { customBefore?: CustomFieldSnapshot | null }).customBefore ?? undefined\n const customAfter = await loadResourceTypeCustomSnapshot(em, after)\n const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [\n 'name',\n 'description',\n 'appearanceIcon',\n 'appearanceColor',\n 'deletedAt',\n ])\n const customChanges = diffCustomFieldChanges(customBefore, customAfter)\n if (Object.keys(customChanges).length) {\n changes.customFields = { from: customBefore ?? null, to: customAfter ?? null }\n }\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.update', 'Update resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: {\n before,\n after,\n customBefore: customBefore ?? null,\n customAfter,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const resourceType = await em.findOne(ResourcesResourceType, { id: before.id })\n if (!resourceType) return\n resourceType.name = before.name\n resourceType.description = before.description ?? null\n resourceType.appearanceIcon = before.appearanceIcon ?? null\n resourceType.appearanceColor = before.appearanceColor ?? null\n resourceType.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null\n resourceType.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n if (payload.customBefore || payload.customAfter) {\n const currentCustom = await loadCustomFieldSnapshot(em, {\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n })\n const reset = buildCustomFieldResetMap(payload.customBefore ?? undefined, currentCustom ?? undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n values: reset,\n })\n }\n\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n },\n}\n\nconst deleteResourceTypeCommand: CommandHandler<{ id?: string }, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.delete',\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Resource type id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadResourceTypeSnapshot(em, id)\n if (!snapshot) return {}\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { before: snapshot, customBefore: custom }\n },\n async execute(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Resource type id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id, deletedAt: null },\n undefined,\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n if (!record) throw new CrudHttpError(404, { error: 'Resources resource type not found.' })\n ensureTenantScope(ctx, record.tenantId)\n ensureOrganizationScope(ctx, record.organizationId)\n const assignedCount = await em.count(ResourcesResource, {\n resourceTypeId: record.id,\n deletedAt: null,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n })\n if (assignedCount > 0) {\n throw new CrudHttpError(400, { error: 'Resource type has assigned resources.' })\n }\n record.deletedAt = new Date()\n record.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as ResourceTypeSnapshot | undefined\n if (!before) return null\n const customBefore = (snapshots as { customBefore?: CustomFieldSnapshot | null }).customBefore ?? undefined\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.delete', 'Delete resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n customBefore: customBefore ?? null,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let resourceType = await em.findOne(ResourcesResourceType, { id: before.id })\n if (!resourceType) {\n resourceType = em.create(ResourcesResourceType, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n name: before.name,\n description: before.description ?? null,\n appearanceIcon: before.appearanceIcon ?? null,\n appearanceColor: before.appearanceColor ?? null,\n deletedAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(resourceType)\n } else {\n resourceType.name = before.name\n resourceType.description = before.description ?? null\n resourceType.appearanceIcon = before.appearanceIcon ?? null\n resourceType.appearanceColor = before.appearanceColor ?? null\n resourceType.deletedAt = null\n resourceType.updatedAt = new Date()\n }\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n if (payload.customBefore) {\n const reset = buildCustomFieldResetMap(payload.customBefore, undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n values: reset,\n })\n }\n\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'created',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n },\n}\n\nregisterCommand(createResourceTypeCommand)\nregisterCommand(updateResourceTypeCommand)\nregisterCommand(deleteResourceTypeCommand)\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, parseWithCustomFields, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { buildCustomFieldResetMap, diffCustomFieldChanges, loadCustomFieldSnapshot, type CustomFieldSnapshot } from '@open-mercato/shared/lib/commands/customFieldSnapshots'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { ResourcesResource, ResourcesResourceType } from '../data/entities'\nimport {\n resourcesResourceTypeCreateSchema,\n resourcesResourceTypeUpdateSchema,\n type ResourcesResourceTypeCreateInput,\n type ResourcesResourceTypeUpdateInput,\n} from '../data/validators'\nimport { resourcesResourceTypeCrudEvents } from '../lib/crud'\nimport { lockResourceType } from '../lib/resourceTypeLock'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'\nimport { E } from '#generated/entities.ids.generated'\n\nconst resourceTypeCrudIndexer: CrudIndexerConfig<ResourcesResourceType> = {\n entityType: E.resources.resources_resource_type,\n}\n\ntype ResourceTypeSnapshot = {\n id: string\n tenantId: string\n organizationId: string\n name: string\n description: string | null\n appearanceIcon: string | null\n appearanceColor: string | null\n deletedAt: string | null\n}\n\ntype ResourceTypeUndoPayload = {\n before?: ResourceTypeSnapshot | null\n after?: ResourceTypeSnapshot | null\n customBefore?: CustomFieldSnapshot | null\n customAfter?: CustomFieldSnapshot | null\n}\n\nasync function loadResourceTypeSnapshot(em: EntityManager, id: string): Promise<ResourceTypeSnapshot | null> {\n const resourceType = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id },\n undefined,\n { tenantId: null, organizationId: null },\n )\n if (!resourceType) return null\n return {\n id: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n name: resourceType.name,\n description: resourceType.description ?? null,\n appearanceIcon: resourceType.appearanceIcon ?? null,\n appearanceColor: resourceType.appearanceColor ?? null,\n deletedAt: resourceType.deletedAt ? resourceType.deletedAt.toISOString() : null,\n }\n}\n\nasync function loadResourceTypeCustomSnapshot(em: EntityManager, snapshot: ResourceTypeSnapshot): Promise<CustomFieldSnapshot> {\n return loadCustomFieldSnapshot(em, {\n entityId: E.resources.resources_resource_type,\n recordId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n })\n}\n\nasync function assertNoAssignedResources(\n em: EntityManager,\n resourceType: ResourcesResourceType,\n status: 400 | 409,\n): Promise<void> {\n const assignedCount = await em.count(ResourcesResource, {\n resourceTypeId: resourceType.id,\n deletedAt: null,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n })\n if (assignedCount === 0) return\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(status, {\n error: translate('resources.resourceTypes.errors.deleteAssigned', 'Resource type has assigned resources.'),\n code: 'resources.resourceTypes.errors.deleteAssigned',\n })\n}\n\nconst createResourceTypeCommand: CommandHandler<ResourcesResourceTypeCreateInput, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.create',\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(resourcesResourceTypeCreateSchema, rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const now = new Date()\n const record = em.create(ResourcesResourceType, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n name: parsed.name,\n description: parsed.description ?? null,\n appearanceIcon: parsed.appearanceIcon ?? null,\n appearanceColor: parsed.appearanceColor ?? null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n await em.flush()\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadResourceTypeSnapshot(em, result.resourceTypeId)\n if (!snapshot) return null\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { snapshot, custom }\n },\n buildLog: async ({ result, ctx }) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadResourceTypeSnapshot(em, result.resourceTypeId)\n if (!snapshot) return null\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.create', 'Create resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: snapshot.id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n snapshotAfter: snapshot,\n payload: {\n undo: {\n after: snapshot,\n customAfter: custom,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const after = payload?.after\n if (!after) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const resourceType = await em.findOne(ResourcesResourceType, { id: after.id })\n if (resourceType) {\n let deleted = false\n await withAtomicFlush(em, [\n async () => {\n await lockResourceType(em, resourceType.id)\n const isLive = await em.count(ResourcesResourceType, {\n id: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n deletedAt: null,\n }) > 0\n if (!isLive) return\n await assertNoAssignedResources(em, resourceType, 409)\n resourceType.deletedAt = new Date()\n resourceType.updatedAt = new Date()\n deleted = true\n },\n ], { transaction: true })\n if (!deleted) return\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'deleted',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n }\n },\n redo: makeCreateRedo<ResourcesResourceType, ResourceTypeSnapshot, ResourcesResourceTypeCreateInput, { resourceTypeId: string }>({\n entityClass: ResourcesResourceType,\n buildResult: (entity) => ({ resourceTypeId: entity.id }),\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n afterRestore: async ({ ctx, entity, logEntry }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n if (!payload?.customAfter) return\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n const reset = buildCustomFieldResetMap(payload.customAfter, undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n values: reset,\n })\n },\n }),\n}\n\nconst updateResourceTypeCommand: CommandHandler<ResourcesResourceTypeUpdateInput, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.update',\n async prepare(rawInput, ctx) {\n const { parsed } = parseWithCustomFields(resourcesResourceTypeUpdateSchema, rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadResourceTypeSnapshot(em, parsed.id)\n if (!snapshot) return {}\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { before: snapshot, customBefore: custom }\n },\n async execute(rawInput, ctx) {\n const { parsed, custom } = parseWithCustomFields(resourcesResourceTypeUpdateSchema, rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id: parsed.id, deletedAt: null },\n undefined,\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n if (!record) throw new CrudHttpError(404, { error: 'Resources resource type not found.' })\n ensureTenantScope(ctx, record.tenantId)\n ensureOrganizationScope(ctx, record.organizationId)\n\n if (parsed.name !== undefined) record.name = parsed.name\n if (parsed.description !== undefined) record.description = parsed.description ?? null\n if (parsed.appearanceIcon !== undefined) record.appearanceIcon = parsed.appearanceIcon ?? null\n if (parsed.appearanceColor !== undefined) record.appearanceColor = parsed.appearanceColor ?? null\n record.updatedAt = new Date()\n\n await em.flush()\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n values: custom,\n })\n\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const before = snapshots.before as ResourceTypeSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const after = await loadResourceTypeSnapshot(em, before.id)\n if (!after) return null\n const customBefore = (snapshots as { customBefore?: CustomFieldSnapshot | null }).customBefore ?? undefined\n const customAfter = await loadResourceTypeCustomSnapshot(em, after)\n const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [\n 'name',\n 'description',\n 'appearanceIcon',\n 'appearanceColor',\n 'deletedAt',\n ])\n const customChanges = diffCustomFieldChanges(customBefore, customAfter)\n if (Object.keys(customChanges).length) {\n changes.customFields = { from: customBefore ?? null, to: customAfter ?? null }\n }\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.update', 'Update resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after,\n changes,\n payload: {\n undo: {\n before,\n after,\n customBefore: customBefore ?? null,\n customAfter,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const resourceType = await em.findOne(ResourcesResourceType, { id: before.id })\n if (!resourceType) return\n resourceType.name = before.name\n resourceType.description = before.description ?? null\n resourceType.appearanceIcon = before.appearanceIcon ?? null\n resourceType.appearanceColor = before.appearanceColor ?? null\n resourceType.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null\n resourceType.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n if (payload.customBefore || payload.customAfter) {\n const currentCustom = await loadCustomFieldSnapshot(em, {\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n })\n const reset = buildCustomFieldResetMap(payload.customBefore ?? undefined, currentCustom ?? undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n values: reset,\n })\n }\n\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n },\n}\n\nconst deleteResourceTypeCommand: CommandHandler<{ id?: string }, { resourceTypeId: string }> = {\n id: 'resources.resourceTypes.delete',\n async prepare(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Resource type id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadResourceTypeSnapshot(em, id)\n if (!snapshot) return {}\n const custom = await loadResourceTypeCustomSnapshot(em, snapshot)\n return { before: snapshot, customBefore: custom }\n },\n async execute(input, ctx) {\n const id = input?.id\n if (!id) throw new CrudHttpError(400, { error: 'Resource type id is required.' })\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const record = await findOneWithDecryption(\n em,\n ResourcesResourceType,\n { id, deletedAt: null },\n undefined,\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },\n )\n if (!record) throw new CrudHttpError(404, { error: 'Resources resource type not found.' })\n ensureTenantScope(ctx, record.tenantId)\n ensureOrganizationScope(ctx, record.organizationId)\n await withAtomicFlush(em, [\n async () => {\n await lockResourceType(em, record.id)\n await assertNoAssignedResources(em, record, 400)\n record.deletedAt = new Date()\n record.updatedAt = new Date()\n },\n ], { transaction: true })\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId,\n tenantId: record.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n return { resourceTypeId: record.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as ResourceTypeSnapshot | undefined\n if (!before) return null\n const customBefore = (snapshots as { customBefore?: CustomFieldSnapshot | null }).customBefore ?? undefined\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('resources.audit.resourceTypes.delete', 'Delete resource type'),\n resourceKind: 'resources.resourceType',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n customBefore: customBefore ?? null,\n } satisfies ResourceTypeUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<ResourceTypeUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let resourceType = await em.findOne(ResourcesResourceType, { id: before.id })\n if (!resourceType) {\n resourceType = em.create(ResourcesResourceType, {\n id: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n name: before.name,\n description: before.description ?? null,\n appearanceIcon: before.appearanceIcon ?? null,\n appearanceColor: before.appearanceColor ?? null,\n deletedAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(resourceType)\n } else {\n resourceType.name = before.name\n resourceType.description = before.description ?? null\n resourceType.appearanceIcon = before.appearanceIcon ?? null\n resourceType.appearanceColor = before.appearanceColor ?? null\n resourceType.deletedAt = null\n resourceType.updatedAt = new Date()\n }\n await em.flush()\n\n const dataEngine = (ctx.container.resolve('dataEngine') as DataEngine)\n if (payload.customBefore) {\n const reset = buildCustomFieldResetMap(payload.customBefore, undefined)\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.resources.resources_resource_type,\n recordId: resourceType.id,\n tenantId: resourceType.tenantId,\n organizationId: resourceType.organizationId,\n values: reset,\n })\n }\n\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'created',\n entity: resourceType,\n identifiers: {\n id: resourceType.id,\n organizationId: resourceType.organizationId,\n tenantId: resourceType.tenantId,\n },\n events: resourcesResourceTypeCrudEvents,\n indexer: resourceTypeCrudIndexer,\n })\n },\n}\n\nregisterCommand(createResourceTypeCommand)\nregisterCommand(updateResourceTypeCommand)\nregisterCommand(deleteResourceTypeCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,12 +7,13 @@ import { buildCustomFieldResetMap, diffCustomFieldChanges, loadCustomFieldSnapsh
|
|
|
7
7
|
import { resolveRedoSnapshot } from "@open-mercato/shared/lib/commands/redo";
|
|
8
8
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
9
|
import { Dictionary, DictionaryEntry } from "@open-mercato/core/modules/dictionaries/data/entities";
|
|
10
|
-
import { ResourcesResource, ResourcesResourceTag, ResourcesResourceTagAssignment } from "../data/entities.js";
|
|
10
|
+
import { ResourcesResource, ResourcesResourceTag, ResourcesResourceTagAssignment, ResourcesResourceType } from "../data/entities.js";
|
|
11
11
|
import {
|
|
12
12
|
resourcesResourceCreateSchema,
|
|
13
13
|
resourcesResourceUpdateSchema
|
|
14
14
|
} from "../data/validators.js";
|
|
15
15
|
import { resourcesResourceCrudEvents } from "../lib/crud.js";
|
|
16
|
+
import { lockResourceType } from "../lib/resourceTypeLock.js";
|
|
16
17
|
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from "./shared.js";
|
|
17
18
|
import { RESOURCES_CAPACITY_UNIT_DICTIONARY_KEY } from "../lib/capacityUnits.js";
|
|
18
19
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
@@ -147,6 +148,50 @@ async function syncResourcesResourceTags(em, params) {
|
|
|
147
148
|
em.persist(assignment);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
151
|
+
async function countResourceTypes(em, resourceTypeId, scope, state) {
|
|
152
|
+
return em.count(ResourcesResourceType, {
|
|
153
|
+
id: resourceTypeId,
|
|
154
|
+
tenantId: scope.tenantId,
|
|
155
|
+
organizationId: scope.organizationId,
|
|
156
|
+
deletedAt: state === "live" ? null : { $ne: null }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
async function assertResourceTypeAssignable(em, resourceTypeId, scope) {
|
|
160
|
+
if (!resourceTypeId) return;
|
|
161
|
+
await lockResourceType(em, resourceTypeId);
|
|
162
|
+
if (await countResourceTypes(em, resourceTypeId, scope, "live") > 0) return;
|
|
163
|
+
throw await resourceTypeUnavailableError(400);
|
|
164
|
+
}
|
|
165
|
+
async function resourceTypeUnavailableError(status) {
|
|
166
|
+
const { translate } = await resolveTranslations();
|
|
167
|
+
return new CrudHttpError(status, {
|
|
168
|
+
error: translate("resources.resources.errors.resourceTypeUnavailable", "The selected resource type is no longer available."),
|
|
169
|
+
code: "resources.resources.errors.resourceTypeUnavailable"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async function assertResourceTypeRestorable(em, record, resourceTypeId, scope) {
|
|
173
|
+
if (!resourceTypeId) return;
|
|
174
|
+
await lockResourceType(em, resourceTypeId);
|
|
175
|
+
if (record && await em.count(ResourcesResource, {
|
|
176
|
+
id: record.id,
|
|
177
|
+
tenantId: scope.tenantId,
|
|
178
|
+
organizationId: scope.organizationId,
|
|
179
|
+
resourceTypeId,
|
|
180
|
+
deletedAt: null
|
|
181
|
+
}) > 0) return;
|
|
182
|
+
if (await countResourceTypes(em, resourceTypeId, scope, "live") > 0) return;
|
|
183
|
+
if (await countResourceTypes(em, resourceTypeId, scope, "deleted") === 0) {
|
|
184
|
+
throw await resourceTypeUnavailableError(409);
|
|
185
|
+
}
|
|
186
|
+
const { translate } = await resolveTranslations();
|
|
187
|
+
throw new CrudHttpError(409, {
|
|
188
|
+
error: translate(
|
|
189
|
+
"resources.resources.errors.resourceTypeDeleted",
|
|
190
|
+
"This would put the resource on a deleted resource type. Restore the resource type first."
|
|
191
|
+
),
|
|
192
|
+
code: "resources.resources.errors.resourceTypeDeleted"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
150
195
|
const createResourceCommand = {
|
|
151
196
|
id: "resources.resources.create",
|
|
152
197
|
async execute(rawInput, ctx) {
|
|
@@ -178,6 +223,7 @@ const createResourceCommand = {
|
|
|
178
223
|
});
|
|
179
224
|
await withAtomicFlush(em, [
|
|
180
225
|
async () => {
|
|
226
|
+
await assertResourceTypeAssignable(em, parsed.resourceTypeId, parsed);
|
|
181
227
|
em.persist(record);
|
|
182
228
|
await em.flush();
|
|
183
229
|
},
|
|
@@ -276,6 +322,7 @@ const createResourceCommand = {
|
|
|
276
322
|
let record = await em.findOne(ResourcesResource, { id: after.id });
|
|
277
323
|
await withAtomicFlush(em, [
|
|
278
324
|
async () => {
|
|
325
|
+
await assertResourceTypeRestorable(em, record, after.resourceTypeId, after);
|
|
279
326
|
if (!record) {
|
|
280
327
|
record = em.create(ResourcesResource, {
|
|
281
328
|
id: after.id,
|
|
@@ -378,6 +425,7 @@ const updateResourceCommand = {
|
|
|
378
425
|
if (!record) throw new CrudHttpError(404, { error: "Resources resource not found." });
|
|
379
426
|
ensureTenantScope(ctx, record.tenantId);
|
|
380
427
|
ensureOrganizationScope(ctx, record.organizationId);
|
|
428
|
+
const resourceTypeChanged = parsed.resourceTypeId !== void 0 && parsed.resourceTypeId !== record.resourceTypeId;
|
|
381
429
|
let capacityUnit;
|
|
382
430
|
if (parsed.capacityUnitValue !== void 0) {
|
|
383
431
|
const unitValue = typeof parsed.capacityUnitValue === "string" ? parsed.capacityUnitValue.trim() : "";
|
|
@@ -388,7 +436,8 @@ const updateResourceCommand = {
|
|
|
388
436
|
) : null;
|
|
389
437
|
}
|
|
390
438
|
await withAtomicFlush(em, [
|
|
391
|
-
() => {
|
|
439
|
+
async () => {
|
|
440
|
+
if (resourceTypeChanged) await assertResourceTypeAssignable(em, parsed.resourceTypeId, record);
|
|
392
441
|
if (parsed.capacityUnitValue !== void 0) {
|
|
393
442
|
record.capacityUnitValue = capacityUnit?.value ?? null;
|
|
394
443
|
record.capacityUnitName = capacityUnit?.name ?? null;
|
|
@@ -499,7 +548,8 @@ const updateResourceCommand = {
|
|
|
499
548
|
const record = await em.findOne(ResourcesResource, { id: before.id });
|
|
500
549
|
if (!record) return;
|
|
501
550
|
await withAtomicFlush(em, [
|
|
502
|
-
() => {
|
|
551
|
+
async () => {
|
|
552
|
+
await assertResourceTypeRestorable(em, record, before.resourceTypeId, before);
|
|
503
553
|
record.name = before.name;
|
|
504
554
|
record.description = before.description ?? null;
|
|
505
555
|
record.resourceTypeId = before.resourceTypeId ?? null;
|
|
@@ -625,6 +675,7 @@ const deleteResourceCommand = {
|
|
|
625
675
|
let record = await em.findOne(ResourcesResource, { id: before.id });
|
|
626
676
|
await withAtomicFlush(em, [
|
|
627
677
|
async () => {
|
|
678
|
+
await assertResourceTypeRestorable(em, record, before.resourceTypeId, before);
|
|
628
679
|
if (!record) {
|
|
629
680
|
record = em.create(ResourcesResource, {
|
|
630
681
|
id: before.id,
|