@open-mercato/core 0.6.7-develop.6725.1.07a748cfc2 → 0.6.7-develop.6744.1.0483b18565
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/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +17 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +7 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +35 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +10 -1
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
2
2
|
import { hasAllFeatures } from "@open-mercato/shared/security/features";
|
|
3
|
+
import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
3
4
|
import { deriveCustomEntityRecordFeature } from "./recordFeatures.js";
|
|
4
5
|
const ENTITY_ACL_REQUIREMENTS = {
|
|
5
6
|
"directory:tenant": {
|
|
@@ -56,9 +57,50 @@ const ENTITY_ACL_REQUIREMENTS = {
|
|
|
56
57
|
manage: ["auth.roles.manage"]
|
|
57
58
|
}
|
|
58
59
|
};
|
|
60
|
+
let declaredCustomEntityRestrictions = null;
|
|
61
|
+
function loadDeclaredCustomEntityRestrictions() {
|
|
62
|
+
if (declaredCustomEntityRestrictions === null) {
|
|
63
|
+
try {
|
|
64
|
+
const modules = getModules();
|
|
65
|
+
const restrictions = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const moduleEntry of modules ?? []) {
|
|
67
|
+
for (const spec of moduleEntry.customEntities ?? []) {
|
|
68
|
+
if (spec.id) restrictions.set(spec.id, spec.accessRestricted === true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
declaredCustomEntityRestrictions = restrictions;
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return declaredCustomEntityRestrictions ?? /* @__PURE__ */ new Map();
|
|
76
|
+
}
|
|
77
|
+
function getDeclaredCustomEntityRestriction(entityId) {
|
|
78
|
+
return loadDeclaredCustomEntityRestrictions().get(entityId);
|
|
79
|
+
}
|
|
80
|
+
function isDeclaredCustomEntity(entityId) {
|
|
81
|
+
return loadDeclaredCustomEntityRestrictions().has(entityId);
|
|
82
|
+
}
|
|
59
83
|
function resolveEntityAclRequirement(entityId) {
|
|
60
84
|
return ENTITY_ACL_REQUIREMENTS[entityId] ?? null;
|
|
61
85
|
}
|
|
86
|
+
function canReadAllEntityMetadata(acl) {
|
|
87
|
+
return Boolean(acl.isSuperAdmin) || hasAllFeatures(acl.features, ["entities.definitions.view"]);
|
|
88
|
+
}
|
|
89
|
+
function canReadEntityMetadata(args) {
|
|
90
|
+
if (args.acl.isSuperAdmin) return true;
|
|
91
|
+
const requirement = resolveEntityAclRequirement(args.entityId);
|
|
92
|
+
if (requirement?.platformOnly) return false;
|
|
93
|
+
if (canReadAllEntityMetadata(args.acl)) return true;
|
|
94
|
+
if (args.isCustomEntity) {
|
|
95
|
+
const requiredFeatures = ["entities.records.view"];
|
|
96
|
+
if (args.isRestricted) {
|
|
97
|
+
requiredFeatures.push(deriveCustomEntityRecordFeature(args.entityId, "view"));
|
|
98
|
+
}
|
|
99
|
+
return hasAllFeatures(args.acl.features, requiredFeatures);
|
|
100
|
+
}
|
|
101
|
+
if (!requirement) return false;
|
|
102
|
+
return hasAllFeatures(args.acl.features, requirement.view);
|
|
103
|
+
}
|
|
62
104
|
function forbiddenEntityAccess() {
|
|
63
105
|
return new CrudHttpError(403, { error: "Forbidden" });
|
|
64
106
|
}
|
|
@@ -90,6 +132,10 @@ async function assertEntityAclForRequest(args) {
|
|
|
90
132
|
}
|
|
91
133
|
export {
|
|
92
134
|
assertEntityAclForRequest,
|
|
135
|
+
canReadAllEntityMetadata,
|
|
136
|
+
canReadEntityMetadata,
|
|
137
|
+
getDeclaredCustomEntityRestriction,
|
|
138
|
+
isDeclaredCustomEntity,
|
|
93
139
|
resolveEntityAclRequirement
|
|
94
140
|
};
|
|
95
141
|
//# sourceMappingURL=entityAcl.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/lib/entityAcl.ts"],
|
|
4
|
-
"sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { deriveCustomEntityRecordFeature } from './recordFeatures'\n\nexport type EntityAclRequirement = {\n view: string[]\n manage: string[]\n platformOnly?: boolean\n}\n\nconst ENTITY_ACL_REQUIREMENTS: Record<string, EntityAclRequirement> = {\n 'directory:tenant': {\n view: ['directory.tenants.view'],\n manage: ['directory.tenants.manage'],\n platformOnly: true,\n },\n 'directory:organization': {\n view: ['directory.organizations.view'],\n manage: ['directory.organizations.manage'],\n },\n 'customers:customer_person_profile': {\n view: ['customers.people.view'],\n manage: ['customers.people.manage'],\n },\n 'customers:customer_company_profile': {\n view: ['customers.companies.view'],\n manage: ['customers.companies.manage'],\n },\n 'customers:customer_deal': {\n view: ['customers.deals.view'],\n manage: ['customers.deals.manage'],\n },\n 'customers:customer_activity': {\n view: ['customers.activities.view'],\n manage: ['customers.activities.manage'],\n },\n 'customers:customer_interaction': {\n view: ['customers.interactions.view'],\n manage: ['customers.interactions.manage'],\n },\n 'catalog:catalog_product': {\n view: ['catalog.products.view'],\n manage: ['catalog.products.manage'],\n },\n 'catalog:catalog_product_category': {\n view: ['catalog.categories.view'],\n manage: ['catalog.categories.manage'],\n },\n 'sales:sales_order': {\n view: ['sales.orders.view'],\n manage: ['sales.orders.manage'],\n },\n 'sales:sales_quote': {\n view: ['sales.quotes.view'],\n manage: ['sales.quotes.manage'],\n },\n 'auth:user': {\n view: ['auth.users.list'],\n manage: ['auth.users.edit'],\n },\n 'auth:role': {\n view: ['auth.roles.list'],\n manage: ['auth.roles.manage'],\n },\n}\n\nexport function resolveEntityAclRequirement(entityId: string): EntityAclRequirement | null {\n return ENTITY_ACL_REQUIREMENTS[entityId] ?? null\n}\n\ntype EntityAclActor = {\n sub?: string | null\n tenantId?: string | null\n orgId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype AssertEntityAclArgs = {\n auth: EntityAclActor\n entityId: string\n action: 'view' | 'manage'\n isCustomEntity: boolean\n // Set for a custom entity flagged `access_restricted`. When true, the coarse\n // route-level entities.records.* feature is no longer sufficient \u2014 the caller\n // must additionally hold the synthesized per-entity feature.\n isRestricted?: boolean\n rbac: RbacService\n}\n\nfunction forbiddenEntityAccess(): CrudHttpError {\n return new CrudHttpError(403, { error: 'Forbidden' })\n}\n\nasync function loadActorAcl(args: AssertEntityAclArgs) {\n return args.rbac.loadAcl(args.auth.sub ?? '', {\n tenantId: args.auth.tenantId ?? null,\n organizationId: args.auth.orgId ?? null,\n })\n}\n\nexport async function assertEntityAclForRequest(args: AssertEntityAclArgs): Promise<void> {\n if (args.isCustomEntity) {\n // Unrestricted custom entities keep the historical behavior: the coarse\n // entities.records.view/.manage route guard is the whole authorization.\n if (!args.isRestricted) return\n\n const acl = await loadActorAcl(args)\n if (acl?.isSuperAdmin) return\n\n const required = deriveCustomEntityRecordFeature(args.entityId, args.action)\n if (!hasAllFeatures(acl?.features, [required])) {\n throw forbiddenEntityAccess()\n }\n return\n }\n\n const requirement = resolveEntityAclRequirement(args.entityId)\n\n const acl = await loadActorAcl(args)\n\n if (acl?.isSuperAdmin) return\n\n if (!requirement) throw forbiddenEntityAccess()\n if (requirement.platformOnly) throw forbiddenEntityAccess()\n\n if (!hasAllFeatures(acl?.features, requirement[args.action])) {\n throw forbiddenEntityAccess()\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAE/B,SAAS,uCAAuC;AAQhD,MAAM,0BAAgE;AAAA,EACpE,oBAAoB;AAAA,IAClB,MAAM,CAAC,wBAAwB;AAAA,IAC/B,QAAQ,CAAC,0BAA0B;AAAA,IACnC,cAAc;AAAA,EAChB;AAAA,EACA,0BAA0B;AAAA,IACxB,MAAM,CAAC,8BAA8B;AAAA,IACrC,QAAQ,CAAC,gCAAgC;AAAA,EAC3C;AAAA,EACA,qCAAqC;AAAA,IACnC,MAAM,CAAC,uBAAuB;AAAA,IAC9B,QAAQ,CAAC,yBAAyB;AAAA,EACpC;AAAA,EACA,sCAAsC;AAAA,IACpC,MAAM,CAAC,0BAA0B;AAAA,IACjC,QAAQ,CAAC,4BAA4B;AAAA,EACvC;AAAA,EACA,2BAA2B;AAAA,IACzB,MAAM,CAAC,sBAAsB;AAAA,IAC7B,QAAQ,CAAC,wBAAwB;AAAA,EACnC;AAAA,EACA,+BAA+B;AAAA,IAC7B,MAAM,CAAC,2BAA2B;AAAA,IAClC,QAAQ,CAAC,6BAA6B;AAAA,EACxC;AAAA,EACA,kCAAkC;AAAA,IAChC,MAAM,CAAC,6BAA6B;AAAA,IACpC,QAAQ,CAAC,+BAA+B;AAAA,EAC1C;AAAA,EACA,2BAA2B;AAAA,IACzB,MAAM,CAAC,uBAAuB;AAAA,IAC9B,QAAQ,CAAC,yBAAyB;AAAA,EACpC;AAAA,EACA,oCAAoC;AAAA,IAClC,MAAM,CAAC,yBAAyB;AAAA,IAChC,QAAQ,CAAC,2BAA2B;AAAA,EACtC;AAAA,EACA,qBAAqB;AAAA,IACnB,MAAM,CAAC,mBAAmB;AAAA,IAC1B,QAAQ,CAAC,qBAAqB;AAAA,EAChC;AAAA,EACA,qBAAqB;AAAA,IACnB,MAAM,CAAC,mBAAmB;AAAA,IAC1B,QAAQ,CAAC,qBAAqB;AAAA,EAChC;AAAA,EACA,aAAa;AAAA,IACX,MAAM,CAAC,iBAAiB;AAAA,IACxB,QAAQ,CAAC,iBAAiB;AAAA,EAC5B;AAAA,EACA,aAAa;AAAA,IACX,MAAM,CAAC,iBAAiB;AAAA,IACxB,QAAQ,CAAC,mBAAmB;AAAA,EAC9B;AACF;AAEO,SAAS,4BAA4B,UAA+C;AACzF,SAAO,wBAAwB,QAAQ,KAAK;AAC9C;AAqBA,SAAS,wBAAuC;AAC9C,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,YAAY,CAAC;AACtD;AAEA,eAAe,aAAa,MAA2B;AACrD,SAAO,KAAK,KAAK,QAAQ,KAAK,KAAK,OAAO,IAAI;AAAA,IAC5C,UAAU,KAAK,KAAK,YAAY;AAAA,IAChC,gBAAgB,KAAK,KAAK,SAAS;AAAA,EACrC,CAAC;AACH;AAEA,eAAsB,0BAA0B,MAA0C;AACxF,MAAI,KAAK,gBAAgB;AAGvB,QAAI,CAAC,KAAK,aAAc;AAExB,UAAMA,OAAM,MAAM,aAAa,IAAI;AACnC,QAAIA,MAAK,aAAc;AAEvB,UAAM,WAAW,gCAAgC,KAAK,UAAU,KAAK,MAAM;AAC3E,QAAI,CAAC,eAAeA,MAAK,UAAU,CAAC,QAAQ,CAAC,GAAG;AAC9C,YAAM,sBAAsB;AAAA,IAC9B;AACA;AAAA,EACF;AAEA,QAAM,cAAc,4BAA4B,KAAK,QAAQ;AAE7D,QAAM,MAAM,MAAM,aAAa,IAAI;AAEnC,MAAI,KAAK,aAAc;AAEvB,MAAI,CAAC,YAAa,OAAM,sBAAsB;AAC9C,MAAI,YAAY,aAAc,OAAM,sBAAsB;AAE1D,MAAI,CAAC,eAAe,KAAK,UAAU,YAAY,KAAK,MAAM,CAAC,GAAG;AAC5D,UAAM,sBAAsB;AAAA,EAC9B;AACF;",
|
|
4
|
+
"sourcesContent": ["import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { deriveCustomEntityRecordFeature } from './recordFeatures'\n\nexport type EntityAclRequirement = {\n view: string[]\n manage: string[]\n platformOnly?: boolean\n}\n\nconst ENTITY_ACL_REQUIREMENTS: Record<string, EntityAclRequirement> = {\n 'directory:tenant': {\n view: ['directory.tenants.view'],\n manage: ['directory.tenants.manage'],\n platformOnly: true,\n },\n 'directory:organization': {\n view: ['directory.organizations.view'],\n manage: ['directory.organizations.manage'],\n },\n 'customers:customer_person_profile': {\n view: ['customers.people.view'],\n manage: ['customers.people.manage'],\n },\n 'customers:customer_company_profile': {\n view: ['customers.companies.view'],\n manage: ['customers.companies.manage'],\n },\n 'customers:customer_deal': {\n view: ['customers.deals.view'],\n manage: ['customers.deals.manage'],\n },\n 'customers:customer_activity': {\n view: ['customers.activities.view'],\n manage: ['customers.activities.manage'],\n },\n 'customers:customer_interaction': {\n view: ['customers.interactions.view'],\n manage: ['customers.interactions.manage'],\n },\n 'catalog:catalog_product': {\n view: ['catalog.products.view'],\n manage: ['catalog.products.manage'],\n },\n 'catalog:catalog_product_category': {\n view: ['catalog.categories.view'],\n manage: ['catalog.categories.manage'],\n },\n 'sales:sales_order': {\n view: ['sales.orders.view'],\n manage: ['sales.orders.manage'],\n },\n 'sales:sales_quote': {\n view: ['sales.quotes.view'],\n manage: ['sales.quotes.manage'],\n },\n 'auth:user': {\n view: ['auth.users.list'],\n manage: ['auth.users.edit'],\n },\n 'auth:role': {\n view: ['auth.roles.list'],\n manage: ['auth.roles.manage'],\n },\n}\n\nlet declaredCustomEntityRestrictions: Map<string, boolean> | null = null\n\nfunction loadDeclaredCustomEntityRestrictions(): Map<string, boolean> {\n if (declaredCustomEntityRestrictions === null) {\n try {\n const modules = getModules() as Array<{\n customEntities?: Array<{ id?: string; accessRestricted?: boolean }>\n }>\n const restrictions = new Map<string, boolean>()\n for (const moduleEntry of modules ?? []) {\n for (const spec of moduleEntry.customEntities ?? []) {\n if (spec.id) restrictions.set(spec.id, spec.accessRestricted === true)\n }\n }\n declaredCustomEntityRestrictions = restrictions\n } catch {}\n }\n return declaredCustomEntityRestrictions ?? new Map<string, boolean>()\n}\n\nexport function getDeclaredCustomEntityRestriction(entityId: string): boolean | undefined {\n return loadDeclaredCustomEntityRestrictions().get(entityId)\n}\n\nexport function isDeclaredCustomEntity(entityId: string): boolean {\n return loadDeclaredCustomEntityRestrictions().has(entityId)\n}\n\nexport function resolveEntityAclRequirement(entityId: string): EntityAclRequirement | null {\n return ENTITY_ACL_REQUIREMENTS[entityId] ?? null\n}\n\nexport function canReadAllEntityMetadata(acl: {\n isSuperAdmin?: boolean\n features?: readonly string[]\n}): boolean {\n return Boolean(acl.isSuperAdmin) || hasAllFeatures(acl.features, ['entities.definitions.view'])\n}\n\nexport function canReadEntityMetadata(args: {\n entityId: string\n isCustomEntity: boolean\n isRestricted?: boolean\n acl: { isSuperAdmin?: boolean; features?: readonly string[] }\n}): boolean {\n if (args.acl.isSuperAdmin) return true\n\n const requirement = resolveEntityAclRequirement(args.entityId)\n if (requirement?.platformOnly) return false\n if (canReadAllEntityMetadata(args.acl)) return true\n if (args.isCustomEntity) {\n const requiredFeatures = ['entities.records.view']\n if (args.isRestricted) {\n requiredFeatures.push(deriveCustomEntityRecordFeature(args.entityId, 'view'))\n }\n return hasAllFeatures(args.acl.features, requiredFeatures)\n }\n if (!requirement) return false\n return hasAllFeatures(args.acl.features, requirement.view)\n}\n\ntype EntityAclActor = {\n sub?: string | null\n tenantId?: string | null\n orgId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype AssertEntityAclArgs = {\n auth: EntityAclActor\n entityId: string\n action: 'view' | 'manage'\n isCustomEntity: boolean\n // Set for a custom entity flagged `access_restricted`. When true, the coarse\n // route-level entities.records.* feature is no longer sufficient \u2014 the caller\n // must additionally hold the synthesized per-entity feature.\n isRestricted?: boolean\n rbac: RbacService\n}\n\nfunction forbiddenEntityAccess(): CrudHttpError {\n return new CrudHttpError(403, { error: 'Forbidden' })\n}\n\nasync function loadActorAcl(args: AssertEntityAclArgs) {\n return args.rbac.loadAcl(args.auth.sub ?? '', {\n tenantId: args.auth.tenantId ?? null,\n organizationId: args.auth.orgId ?? null,\n })\n}\n\nexport async function assertEntityAclForRequest(args: AssertEntityAclArgs): Promise<void> {\n if (args.isCustomEntity) {\n // Unrestricted custom entities keep the historical behavior: the coarse\n // entities.records.view/.manage route guard is the whole authorization.\n if (!args.isRestricted) return\n\n const acl = await loadActorAcl(args)\n if (acl?.isSuperAdmin) return\n\n const required = deriveCustomEntityRecordFeature(args.entityId, args.action)\n if (!hasAllFeatures(acl?.features, [required])) {\n throw forbiddenEntityAccess()\n }\n return\n }\n\n const requirement = resolveEntityAclRequirement(args.entityId)\n\n const acl = await loadActorAcl(args)\n\n if (acl?.isSuperAdmin) return\n\n if (!requirement) throw forbiddenEntityAccess()\n if (requirement.platformOnly) throw forbiddenEntityAccess()\n\n if (!hasAllFeatures(acl?.features, requirement[args.action])) {\n throw forbiddenEntityAccess()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["acl"]
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import { getMessageType } from "../lib/message-types-registry.js";
|
|
|
10
10
|
import { validateMessageObjectsForType } from "../lib/object-validation.js";
|
|
11
11
|
import { attachOperationMetadataHeader } from "../lib/operationMetadata.js";
|
|
12
12
|
import { canUseMessageEmailFeature, resolveMessageContext } from "../lib/routeHelpers.js";
|
|
13
|
+
import { applyMessageParticipantScope } from "../lib/participantScope.js";
|
|
13
14
|
import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from "./guards.js";
|
|
14
15
|
import { findMessageIdsBySearchTokens } from "../lib/searchLookup.js";
|
|
15
16
|
import {
|
|
@@ -67,11 +68,7 @@ async function GET(req) {
|
|
|
67
68
|
joinRecipient();
|
|
68
69
|
break;
|
|
69
70
|
case "all":
|
|
70
|
-
|
|
71
|
-
q = q.where((eb) => eb.or([
|
|
72
|
-
eb("m.sender_user_id", "=", scope.userId),
|
|
73
|
-
eb("r.message_id", "is not", null)
|
|
74
|
-
]));
|
|
71
|
+
q = applyMessageParticipantScope(q, scope.userId);
|
|
75
72
|
break;
|
|
76
73
|
default: {
|
|
77
74
|
const unsupportedFolder = input.folder;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/api/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n joinRecipient()\n q = q.where((eb: any) => eb.or([\n eb('m.sender_user_id', '=', scope.userId),\n eb('r.message_id', 'is not', null),\n ]))\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n // Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp \u2014\n // not in the messages:message encryption map whose encrypted fields are:\n // subject, body, external_email, external_name, action_data, action_result).\n // The handler already uses the correct two-phase shape: Kysely SQL\n // ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then\n // findWithDecryption is called only for those IDs \u2014 never for the full\n // result set. The #3278 unbounded-decrypt hazard does not apply here.\n // Covered by __tests__/list.test.ts.\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n return Response.json({\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n })\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAsB,WAAW;AAGjC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,YAAY;AACrB,SAAS,SAAS,qBAAqB;AACvC,SAAS,sBAAsB,0BAA0B;AACzD,SAAS,oCAAoC;AAC7C,SAAS,sBAAsB;AAC/B,SAAS,qCAAqC;AAC9C,SAAS,qCAAqC;AAC9C,SAAS,2BAA2B,6BAA6B;AACjE,SAAS,qBAAqB,qCAAqC,gCAAgC;AACnG,SAAS,oCAAoC;AAE7C;AAAA,EACE,wBAAwB;AAAA,EACxB;AAAA,EACA,sBAAsB;AAAA,EACtB;AAAA,OACK;AAMP,MAAM,cAAc;AAEpB,SAAS,MAAM,IAAgC;AAC7C,SAAO,GAAG,UAAe;AAC3B;AAoBO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,KAAK;AAAA,EACzB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AACnE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,KAAK,MAAM,IAAI,MAAM,sBAAsB,GAAG;AACtD,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,OAAO,YAAY,IAAI,YAAY;AAClD,QAAM,QAAQ,mBAAmB,MAAM,MAAM;AAC7C,QAAM,KAAK,MAAM,EAAE;AAEnB,QAAM,YAAY,MAAM,SACpB,MAAM,6BAA6B;AAAA,IACjC;AAAA,IACA,OAAO,MAAM;AAAA,IACb,UAAU,MAAM,YAAY;AAAA,IAC5B,gBAAgB,MAAM;AAAA,EACxB,CAAC,IACD;AAEJ,QAAM,iBAAiB,MAAM;AAC3B,QAAI,IAAS,GACV,WAAW,eAAe,EAC1B,MAAM,eAAe,KAAK,MAAM,QAAQ,EACxC,MAAM,gBAAgB,MAAM,IAAI;AAEnC,QAAI,MAAM,gBAAgB;AACxB,UAAI,EAAE,MAAM,qBAAqB,KAAK,MAAM,cAAc;AAAA,IAC5D,OAAO;AACL,UAAI,EAAE,MAAM,qBAAqB,MAAM,IAAI;AAAA,IAC7C;AAEA,UAAM,gBAAgB,MAAM;AAC1B,UAAI,EAAE,SAAS,2BAA2B,CAAC,OAAY,GACpD,MAAM,QAAQ,KAAK,cAAc,EACjC,GAAG,uBAAuB,KAAK,MAAM,MAAM,CAAC;AAAA,IACjD;AAEA,YAAQ,MAAM,QAAQ;AAAA,MACpB,KAAK;AACH,sBAAc;AACd,YAAI,EACD,MAAM,gBAAgB,UAAU,IAAI,EACpC,MAAM,gBAAgB,MAAM,IAAI,EAChC,MAAM,iBAAiB,MAAM,IAAI,EACjC,MAAM,cAAc,KAAK,KAAK;AACjC;AAAA,MACF,KAAK;AACH,sBAAc;AACd,YAAI,EACD,MAAM,gBAAgB,UAAU,IAAI,EACpC,MAAM,gBAAgB,MAAM,IAAI,EAChC,MAAM,iBAAiB,UAAU,IAAI;AACxC;AAAA,MACF,KAAK;AACH,YAAI,EACD,MAAM,oBAAoB,KAAK,MAAM,MAAM,EAC3C,MAAM,cAAc,KAAK,KAAK;AACjC,sBAAc;AACd;AAAA,MACF,KAAK;AACH,YAAI,EACD,MAAM,oBAAoB,KAAK,MAAM,MAAM,EAC3C,MAAM,cAAc,KAAK,IAAI;AAChC,sBAAc;AACd;AAAA,MACF,KAAK;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { applyMessageParticipantScope } from '../lib/participantScope'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n // Sender-OR-recipient participant scope shared with the\n // communication_channels message enricher \u2014 see participantScope.ts (#4133).\n q = applyMessageParticipantScope(q, scope.userId)\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n // Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp \u2014\n // not in the messages:message encryption map whose encrypted fields are:\n // subject, body, external_email, external_name, action_data, action_result).\n // The handler already uses the correct two-phase shape: Kysely SQL\n // ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then\n // findWithDecryption is called only for those IDs \u2014 never for the full\n // result set. The #3278 unbounded-decrypt hazard does not apply here.\n // Covered by __tests__/list.test.ts.\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n return Response.json({\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n })\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function applyMessageParticipantScope(query, userId) {
|
|
2
|
+
return query.leftJoin(
|
|
3
|
+
"message_recipients as r",
|
|
4
|
+
(join) => join.onRef("m.id", "=", "r.message_id").on("r.recipient_user_id", "=", userId).on("r.deleted_at", "is", null)
|
|
5
|
+
).where(
|
|
6
|
+
(eb) => eb.or([
|
|
7
|
+
eb("m.sender_user_id", "=", userId),
|
|
8
|
+
eb("r.message_id", "is not", null)
|
|
9
|
+
])
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
applyMessageParticipantScope
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=participantScope.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/messages/lib/participantScope.ts"],
|
|
4
|
+
"sourcesContent": ["import type { SelectQueryBuilder } from 'kysely'\n\n/**\n * Minimal Kysely schema contract the message participant-scope predicate depends\n * on. Both the messages list route (`api/route.ts`, `all` folder) and the\n * `communication_channels.message-channel` response enricher build their\n * sender-OR-recipient access filter from {@link applyMessageParticipantScope},\n * so a column rename (`sender_user_id`, `recipient_user_id`, `deleted_at`) or a\n * change to the recipient-visibility rules updates both call sites at once\n * instead of silently desyncing the enricher's security boundary from the list\n * route (#4133, follow-up to #4099).\n */\nexport type MessagesParticipantScopeDatabase = {\n messages: {\n id: string\n tenant_id: string\n organization_id: string | null\n sender_user_id: string\n deleted_at: Date | null\n }\n message_recipients: {\n message_id: string\n recipient_user_id: string\n deleted_at: Date | null\n }\n}\n\ntype MessagesTable = MessagesParticipantScopeDatabase['messages']\ntype MessageRecipientsTable = MessagesParticipantScopeDatabase['message_recipients']\n\ntype MessagesFrom = MessagesParticipantScopeDatabase & { m: MessagesTable }\ntype MessagesJoinedFrom = MessagesFrom & { r: MessageRecipientsTable }\n\n/**\n * Apply the shared message participant-scope predicate to a query already built\n * from `messages as m`. A message is visible to `userId` when they are the\n * sender OR a non-deleted recipient. The recipient soft-delete rule\n * (`r.deleted_at is null`) lives inside the recipient join, so it is part of the\n * single source of truth \u2014 the recipient-visibility boundary cannot drift\n * between the list route and the enricher.\n *\n * Message-level tenant / organization / soft-delete scoping stays with the\n * caller (both call sites already apply it uniformly to every query), but the\n * shared {@link MessagesParticipantScopeDatabase} type keeps those column names\n * coupled at compile time as well.\n */\nexport function applyMessageParticipantScope<O>(\n query: SelectQueryBuilder<MessagesFrom, 'm', O>,\n userId: string,\n): SelectQueryBuilder<MessagesJoinedFrom, 'm' | 'r', O> {\n return query\n .leftJoin('message_recipients as r', (join) =>\n join\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', userId)\n .on('r.deleted_at', 'is', null),\n )\n .where((eb) =>\n eb.or([\n eb('m.sender_user_id', '=', userId),\n eb('r.message_id', 'is not', null),\n ]),\n ) as unknown as SelectQueryBuilder<MessagesJoinedFrom, 'm' | 'r', O>\n}\n"],
|
|
5
|
+
"mappings": "AA8CO,SAAS,6BACd,OACA,QACsD;AACtD,SAAO,MACJ;AAAA,IAAS;AAAA,IAA2B,CAAC,SACpC,KACG,MAAM,QAAQ,KAAK,cAAc,EACjC,GAAG,uBAAuB,KAAK,MAAM,EACrC,GAAG,gBAAgB,MAAM,IAAI;AAAA,EAClC,EACC;AAAA,IAAM,CAAC,OACN,GAAG,GAAG;AAAA,MACJ,GAAG,oBAAoB,KAAK,MAAM;AAAA,MAClC,GAAG,gBAAgB,UAAU,IAAI;AAAA,IACnC,CAAC;AAAA,EACH;AACJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
3
|
+
import { fetchAllDictionaryEntries } from "@open-mercato/core/modules/dictionaries/lib/fetchAllEntries";
|
|
3
4
|
import {
|
|
4
5
|
resolveUnavailabilityReasonDictionary
|
|
5
6
|
} from "@open-mercato/core/modules/planner/lib/unavailabilityReasons";
|
|
@@ -25,10 +26,9 @@ async function loadUnavailabilityReasonEntries(subjectType) {
|
|
|
25
26
|
const dictionary = resolveUnavailabilityReasonDictionary(subjectType);
|
|
26
27
|
const resolved = await ensureDictionary(dictionary.key, dictionary.name);
|
|
27
28
|
if (!resolved) return [];
|
|
28
|
-
const entriesCall = await
|
|
29
|
+
const entriesCall = await fetchAllDictionaryEntries(resolved.id);
|
|
29
30
|
if (!entriesCall.ok) return [];
|
|
30
|
-
|
|
31
|
-
return items.map((entry) => {
|
|
31
|
+
return entriesCall.items.map((entry) => {
|
|
32
32
|
if (!entry || typeof entry !== "object") return null;
|
|
33
33
|
const record = entry;
|
|
34
34
|
const id = typeof record.id === "string" ? record.id : null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/planner/components/unavailabilityReasons.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport {\n resolveUnavailabilityReasonDictionary,\n type UnavailabilityReasonSubjectType,\n} from '@open-mercato/core/modules/planner/lib/unavailabilityReasons'\n\nexport type UnavailabilityReasonEntry = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\ntype DictionarySummary = {\n id: string\n key: string\n name: string\n}\n\nasync function ensureDictionary(key: string, name: string): Promise<DictionarySummary | null> {\n const listCall = await apiCall<{ items?: DictionarySummary[] }>('/api/dictionaries')\n const items = Array.isArray(listCall.result?.items) ? listCall.result?.items ?? [] : []\n const existing = items.find((item) => item && item.key === key)\n if (existing) return existing\n if (!listCall.ok) return null\n const created = await apiCallOrThrow<DictionarySummary>(\n '/api/dictionaries',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ key, name }),\n },\n )\n const result = created.result as DictionarySummary | undefined\n if (result && result.id && result.key) return result\n return null\n}\n\nexport async function loadUnavailabilityReasonEntries(\n subjectType: UnavailabilityReasonSubjectType,\n): Promise<UnavailabilityReasonEntry[]> {\n const dictionary = resolveUnavailabilityReasonDictionary(subjectType)\n const resolved = await ensureDictionary(dictionary.key, dictionary.name)\n if (!resolved) return []\n const entriesCall = await
|
|
5
|
-
"mappings": ";AAEA,SAAS,SAAS,sBAAsB;AACxC;AAAA,EACE;AAAA,OAEK;AAgBP,eAAe,iBAAiB,KAAa,MAAiD;AAC5F,QAAM,WAAW,MAAM,QAAyC,mBAAmB;AACnF,QAAM,QAAQ,MAAM,QAAQ,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,SAAS,CAAC,IAAI,CAAC;AACtF,QAAM,WAAW,MAAM,KAAK,CAAC,SAAS,QAAQ,KAAK,QAAQ,GAAG;AAC9D,MAAI,SAAU,QAAO;AACrB,MAAI,CAAC,SAAS,GAAI,QAAO;AACzB,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,KAAK,KAAK,CAAC;AAAA,IACpC;AAAA,EACF;AACA,QAAM,SAAS,QAAQ;AACvB,MAAI,UAAU,OAAO,MAAM,OAAO,IAAK,QAAO;AAC9C,SAAO;AACT;AAEA,eAAsB,gCACpB,aACsC;AACtC,QAAM,aAAa,sCAAsC,WAAW;AACpE,QAAM,WAAW,MAAM,iBAAiB,WAAW,KAAK,WAAW,IAAI;AACvE,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,QAAM,cAAc,MAAM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { fetchAllDictionaryEntries } from '@open-mercato/core/modules/dictionaries/lib/fetchAllEntries'\nimport {\n resolveUnavailabilityReasonDictionary,\n type UnavailabilityReasonSubjectType,\n} from '@open-mercato/core/modules/planner/lib/unavailabilityReasons'\n\nexport type UnavailabilityReasonEntry = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\ntype DictionarySummary = {\n id: string\n key: string\n name: string\n}\n\nasync function ensureDictionary(key: string, name: string): Promise<DictionarySummary | null> {\n const listCall = await apiCall<{ items?: DictionarySummary[] }>('/api/dictionaries')\n const items = Array.isArray(listCall.result?.items) ? listCall.result?.items ?? [] : []\n const existing = items.find((item) => item && item.key === key)\n if (existing) return existing\n if (!listCall.ok) return null\n const created = await apiCallOrThrow<DictionarySummary>(\n '/api/dictionaries',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ key, name }),\n },\n )\n const result = created.result as DictionarySummary | undefined\n if (result && result.id && result.key) return result\n return null\n}\n\nexport async function loadUnavailabilityReasonEntries(\n subjectType: UnavailabilityReasonSubjectType,\n): Promise<UnavailabilityReasonEntry[]> {\n const dictionary = resolveUnavailabilityReasonDictionary(subjectType)\n const resolved = await ensureDictionary(dictionary.key, dictionary.name)\n if (!resolved) return []\n const entriesCall = await fetchAllDictionaryEntries(resolved.id)\n if (!entriesCall.ok) return []\n return entriesCall.items\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const record = entry as Record<string, unknown>\n const id = typeof record.id === 'string' ? record.id : null\n const value = typeof record.value === 'string' ? record.value : null\n if (!id || !value) return null\n const label = typeof record.label === 'string' && record.label.trim().length ? record.label : value\n const color = typeof record.color === 'string' ? record.color : null\n const icon = typeof record.icon === 'string' ? record.icon : null\n return { id, value, label, color, icon }\n })\n .filter((entry): entry is UnavailabilityReasonEntry => !!entry)\n}\n\nexport async function createUnavailabilityReasonEntry(\n subjectType: UnavailabilityReasonSubjectType,\n input: { value: string; label?: string; color?: string | null; icon?: string | null },\n): Promise<UnavailabilityReasonEntry | null> {\n const dictionary = resolveUnavailabilityReasonDictionary(subjectType)\n const resolved = await ensureDictionary(dictionary.key, dictionary.name)\n if (!resolved) return null\n const response = await apiCallOrThrow<Record<string, unknown>>(\n `/api/dictionaries/${resolved.id}/entries`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n value: input.value,\n label: input.label ?? undefined,\n color: input.color ?? undefined,\n icon: input.icon ?? undefined,\n }),\n },\n )\n const payload = response.result ?? {}\n const id = typeof payload.id === 'string' ? payload.id : null\n const value = typeof payload.value === 'string' && payload.value.trim().length ? payload.value : input.value\n const label = typeof payload.label === 'string' && payload.label.trim().length ? payload.label : value\n const color = typeof payload.color === 'string' ? payload.color : input.color ?? null\n const icon = typeof payload.icon === 'string' ? payload.icon : input.icon ?? null\n if (!id) return null\n return { id, value, label, color, icon }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2
2
|
const STALE_JOB_TIMEOUT_SECONDS = 60;
|
|
3
|
+
const STALE_PENDING_TIMEOUT_SECONDS = 900;
|
|
3
4
|
function calculateEta(processedCount, totalCount, startedAt) {
|
|
4
5
|
if (processedCount === 0 || totalCount === 0) return null;
|
|
5
6
|
const elapsedMs = Date.now() - startedAt.getTime();
|
|
@@ -15,6 +16,7 @@ function calculateProgressPercent(processedCount, totalCount) {
|
|
|
15
16
|
export {
|
|
16
17
|
HEARTBEAT_INTERVAL_MS,
|
|
17
18
|
STALE_JOB_TIMEOUT_SECONDS,
|
|
19
|
+
STALE_PENDING_TIMEOUT_SECONDS,
|
|
18
20
|
calculateEta,
|
|
19
21
|
calculateProgressPercent
|
|
20
22
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/progress/lib/progressService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ProgressJob } from '../data/entities'\nimport type { CreateProgressJobInput, UpdateProgressInput, CompleteJobInput, FailJobInput } from '../data/validators'\n\nexport interface ProgressServiceContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface ProgressService {\n createJob(input: CreateProgressJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n startJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n updateProgress(jobId: string, input: UpdateProgressInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n incrementProgress(jobId: string, delta: number, ctx: ProgressServiceContext): Promise<ProgressJob>\n completeJob(jobId: string, input: CompleteJobInput | undefined, ctx: ProgressServiceContext): Promise<ProgressJob>\n failJob(jobId: string, input: FailJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n cancelJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n markCancelled(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n isCancellationRequested(jobId: string, tenantId: string, organizationId?: string | null): Promise<boolean>\n getActiveJobs(ctx: ProgressServiceContext): Promise<ProgressJob[]>\n getRecentlyCompletedJobs(ctx: ProgressServiceContext, sinceSeconds?: number): Promise<ProgressJob[]>\n getJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null>\n markStaleJobsFailed(tenantId: string, timeoutSeconds?: number, organizationId?: string | null): Promise<number>\n}\n\nexport const HEARTBEAT_INTERVAL_MS = 5000\nexport const STALE_JOB_TIMEOUT_SECONDS = 60\n\nexport function calculateEta(\n processedCount: number,\n totalCount: number,\n startedAt: Date,\n): number | null {\n if (processedCount === 0 || totalCount === 0) return null\n\n const elapsedMs = Date.now() - startedAt.getTime()\n const rate = processedCount / elapsedMs\n const remaining = totalCount - processedCount\n\n if (rate <= 0) return null\n\n return Math.ceil(remaining / rate / 1000)\n}\n\nexport function calculateProgressPercent(processedCount: number, totalCount: number | null): number {\n if (!totalCount || totalCount <= 0) return 0\n return Math.min(100, Math.round((processedCount / totalCount) * 100))\n}\n"],
|
|
5
|
-
"mappings": "AAyBO,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import type { ProgressJob } from '../data/entities'\nimport type { CreateProgressJobInput, UpdateProgressInput, CompleteJobInput, FailJobInput } from '../data/validators'\n\nexport interface ProgressServiceContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface ProgressService {\n createJob(input: CreateProgressJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n startJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n updateProgress(jobId: string, input: UpdateProgressInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n incrementProgress(jobId: string, delta: number, ctx: ProgressServiceContext): Promise<ProgressJob>\n completeJob(jobId: string, input: CompleteJobInput | undefined, ctx: ProgressServiceContext): Promise<ProgressJob>\n failJob(jobId: string, input: FailJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>\n cancelJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n markCancelled(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>\n isCancellationRequested(jobId: string, tenantId: string, organizationId?: string | null): Promise<boolean>\n getActiveJobs(ctx: ProgressServiceContext): Promise<ProgressJob[]>\n getRecentlyCompletedJobs(ctx: ProgressServiceContext, sinceSeconds?: number): Promise<ProgressJob[]>\n getJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null>\n markStaleJobsFailed(tenantId: string, timeoutSeconds?: number, organizationId?: string | null): Promise<number>\n}\n\nexport const HEARTBEAT_INTERVAL_MS = 5000\nexport const STALE_JOB_TIMEOUT_SECONDS = 60\nexport const STALE_PENDING_TIMEOUT_SECONDS = 900\n\nexport function calculateEta(\n processedCount: number,\n totalCount: number,\n startedAt: Date,\n): number | null {\n if (processedCount === 0 || totalCount === 0) return null\n\n const elapsedMs = Date.now() - startedAt.getTime()\n const rate = processedCount / elapsedMs\n const remaining = totalCount - processedCount\n\n if (rate <= 0) return null\n\n return Math.ceil(remaining / rate / 1000)\n}\n\nexport function calculateProgressPercent(processedCount: number, totalCount: number | null): number {\n if (!totalCount || totalCount <= 0) return 0\n return Math.min(100, Math.round((processedCount / totalCount) * 100))\n}\n"],
|
|
5
|
+
"mappings": "AAyBO,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAClC,MAAM,gCAAgC;AAEtC,SAAS,aACd,gBACA,YACA,WACe;AACf,MAAI,mBAAmB,KAAK,eAAe,EAAG,QAAO;AAErD,QAAM,YAAY,KAAK,IAAI,IAAI,UAAU,QAAQ;AACjD,QAAM,OAAO,iBAAiB;AAC9B,QAAM,YAAY,aAAa;AAE/B,MAAI,QAAQ,EAAG,QAAO;AAEtB,SAAO,KAAK,KAAK,YAAY,OAAO,GAAI;AAC1C;AAEO,SAAS,yBAAyB,gBAAwB,YAAmC;AAClG,MAAI,CAAC,cAAc,cAAc,EAAG,QAAO;AAC3C,SAAO,KAAK,IAAI,KAAK,KAAK,MAAO,iBAAiB,aAAc,GAAG,CAAC;AACtE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|