@open-mercato/core 0.4.11-develop.1354.54d40d164a → 0.4.11-develop.1362.574a071900
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/dist/modules/customers/api/companies/route.js +141 -3
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +52 -3
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +145 -3
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +195 -0
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +171 -6
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +100 -7
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +180 -7
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +7 -0
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +1 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +81 -1
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/customers/api/companies/route.ts +151 -3
- package/src/modules/customers/api/deals/route.ts +54 -3
- package/src/modules/customers/api/people/route.ts +160 -3
- package/src/modules/customers/api/utils.ts +286 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +184 -9
- package/src/modules/customers/backend/customers/deals/page.tsx +127 -35
- package/src/modules/customers/backend/customers/people/page.tsx +191 -10
- package/src/modules/customers/commands/interactions.ts +7 -0
- package/src/modules/customers/components/detail/DealForm.tsx +1 -0
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +15 -3
- package/src/modules/customers/i18n/es.json +12 -0
- package/src/modules/customers/i18n/pl.json +12 -0
- package/src/modules/query_index/lib/engine.ts +95 -1
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
3
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
-
import { CustomerEntity } from "../../data/entities.js";
|
|
4
|
+
import { CustomerEntity, CustomerPersonProfile } from "../../data/entities.js";
|
|
5
5
|
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
6
6
|
import { personCreateSchema, personUpdateSchema } from "../../data/validators.js";
|
|
7
7
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
applyEntityIdRestriction,
|
|
10
|
+
consumeAdvancedFilterState,
|
|
11
|
+
findMatchingEntityIdsWithQueryEngine,
|
|
12
|
+
findMatchingEntityIdsBySearchTokensAcrossSources,
|
|
13
|
+
withScopedPayload
|
|
14
|
+
} from "../utils.js";
|
|
9
15
|
import { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
10
16
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
11
17
|
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
18
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
19
|
+
import { mergeAdvancedFilters } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
|
|
12
20
|
import {
|
|
13
21
|
createCustomersCrudOpenApi,
|
|
14
22
|
createPagedListResponseSchema,
|
|
@@ -82,10 +90,66 @@ const crud = makeCrudRoute({
|
|
|
82
90
|
updatedAt: "updated_at"
|
|
83
91
|
},
|
|
84
92
|
buildFilters: async (query, ctx) => {
|
|
93
|
+
const advancedQuery = { ...query };
|
|
94
|
+
const advancedFilterState = consumeAdvancedFilterState(query);
|
|
85
95
|
const filters = { kind: { $eq: "person" } };
|
|
86
96
|
if (query.id) filters.id = { $eq: query.id };
|
|
87
97
|
if (query.search) {
|
|
88
|
-
|
|
98
|
+
const matchingIds = ctx ? await findMatchingEntityIdsBySearchTokensAcrossSources({
|
|
99
|
+
ctx,
|
|
100
|
+
query: query.search,
|
|
101
|
+
sources: [
|
|
102
|
+
{
|
|
103
|
+
entityType: E.customers.customer_entity,
|
|
104
|
+
fields: [
|
|
105
|
+
"display_name",
|
|
106
|
+
"primary_email",
|
|
107
|
+
"primary_phone",
|
|
108
|
+
"description",
|
|
109
|
+
"status",
|
|
110
|
+
"lifecycle_stage",
|
|
111
|
+
"source",
|
|
112
|
+
"next_interaction_name"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
entityType: E.customers.customer_person_profile,
|
|
117
|
+
fields: [
|
|
118
|
+
"display_name",
|
|
119
|
+
"primary_email",
|
|
120
|
+
"primary_phone",
|
|
121
|
+
"status",
|
|
122
|
+
"lifecycle_stage",
|
|
123
|
+
"source",
|
|
124
|
+
"first_name",
|
|
125
|
+
"last_name",
|
|
126
|
+
"preferred_name",
|
|
127
|
+
"job_title",
|
|
128
|
+
"department",
|
|
129
|
+
"seniority",
|
|
130
|
+
"timezone",
|
|
131
|
+
"linked_in_url",
|
|
132
|
+
"twitter_url"
|
|
133
|
+
],
|
|
134
|
+
mapToEntityIds: {
|
|
135
|
+
table: "customer_people",
|
|
136
|
+
targetColumn: "entity_id"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}) : null;
|
|
141
|
+
if (matchingIds !== null && matchingIds.length > 0) {
|
|
142
|
+
applyEntityIdRestriction(filters, matchingIds);
|
|
143
|
+
} else {
|
|
144
|
+
const searchPattern = `%${escapeLikePattern(query.search)}%`;
|
|
145
|
+
filters.$or = [
|
|
146
|
+
{ display_name: { $ilike: searchPattern } },
|
|
147
|
+
{ primary_email: { $ilike: searchPattern } },
|
|
148
|
+
{ primary_phone: { $ilike: searchPattern } },
|
|
149
|
+
{ description: { $ilike: searchPattern } },
|
|
150
|
+
{ next_interaction_name: { $ilike: searchPattern } }
|
|
151
|
+
];
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
154
|
const email = typeof query.email === "string" ? query.email.trim().toLowerCase() : "";
|
|
91
155
|
const emailStartsWith = typeof query.emailStartsWith === "string" ? query.emailStartsWith.trim().toLowerCase() : "";
|
|
@@ -151,6 +215,36 @@ const crud = makeCrudRoute({
|
|
|
151
215
|
} catch {
|
|
152
216
|
}
|
|
153
217
|
}
|
|
218
|
+
if (ctx && advancedFilterState) {
|
|
219
|
+
const advancedFilters = mergeAdvancedFilters(
|
|
220
|
+
{ ...filters },
|
|
221
|
+
advancedQuery
|
|
222
|
+
);
|
|
223
|
+
const matchedIds = await findMatchingEntityIdsWithQueryEngine({
|
|
224
|
+
ctx,
|
|
225
|
+
entityId: E.customers.customer_entity,
|
|
226
|
+
filters: advancedFilters,
|
|
227
|
+
customFieldSources: [
|
|
228
|
+
{
|
|
229
|
+
entityId: E.customers.customer_person_profile,
|
|
230
|
+
table: "customer_people",
|
|
231
|
+
alias: "person_profile",
|
|
232
|
+
recordIdColumn: "id",
|
|
233
|
+
join: { fromField: "id", toField: "entity_id" }
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
joins: [
|
|
237
|
+
{
|
|
238
|
+
alias: "tag_assignments",
|
|
239
|
+
table: "customer_tag_assignments",
|
|
240
|
+
from: { field: "id" },
|
|
241
|
+
to: { field: "entity_id" },
|
|
242
|
+
type: "left"
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
});
|
|
246
|
+
applyEntityIdRestriction(filters, matchedIds);
|
|
247
|
+
}
|
|
154
248
|
return filters;
|
|
155
249
|
},
|
|
156
250
|
customFieldSources: [
|
|
@@ -224,6 +318,54 @@ const crud = makeCrudRoute({
|
|
|
224
318
|
},
|
|
225
319
|
response: () => ({ ok: true })
|
|
226
320
|
}
|
|
321
|
+
},
|
|
322
|
+
hooks: {
|
|
323
|
+
afterList: async (payload, ctx) => {
|
|
324
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
325
|
+
const ids = items.map((item) => item && typeof item === "object" && typeof item.id === "string" ? item.id : null).filter((id) => typeof id === "string" && id.length > 0);
|
|
326
|
+
if (!ids.length) return;
|
|
327
|
+
const where = {
|
|
328
|
+
entity: { $in: ids },
|
|
329
|
+
tenantId: ctx.auth?.tenantId ?? null
|
|
330
|
+
};
|
|
331
|
+
if (ctx.selectedOrganizationId) {
|
|
332
|
+
where.organizationId = ctx.selectedOrganizationId;
|
|
333
|
+
}
|
|
334
|
+
const profiles = await findWithDecryption(
|
|
335
|
+
ctx.container.resolve("em"),
|
|
336
|
+
CustomerPersonProfile,
|
|
337
|
+
where,
|
|
338
|
+
{ populate: ["entity", "company"] },
|
|
339
|
+
{
|
|
340
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
341
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
const profilesByEntityId = /* @__PURE__ */ new Map();
|
|
345
|
+
for (const profile of profiles) {
|
|
346
|
+
const entityId = typeof profile?.entity?.id === "string" ? profile.entity.id : null;
|
|
347
|
+
if (entityId) profilesByEntityId.set(entityId, profile);
|
|
348
|
+
}
|
|
349
|
+
payload.items = items.map((item) => {
|
|
350
|
+
if (!item || typeof item !== "object") return item;
|
|
351
|
+
const record = item;
|
|
352
|
+
const profile = typeof record.id === "string" ? profilesByEntityId.get(record.id) : void 0;
|
|
353
|
+
if (!profile) return item;
|
|
354
|
+
return {
|
|
355
|
+
...record,
|
|
356
|
+
first_name: profile.firstName ?? null,
|
|
357
|
+
last_name: profile.lastName ?? null,
|
|
358
|
+
preferred_name: profile.preferredName ?? null,
|
|
359
|
+
job_title: profile.jobTitle ?? null,
|
|
360
|
+
department: profile.department ?? null,
|
|
361
|
+
seniority: profile.seniority ?? null,
|
|
362
|
+
timezone: profile.timezone ?? null,
|
|
363
|
+
linked_in_url: profile.linkedInUrl ?? null,
|
|
364
|
+
twitter_url: profile.twitterUrl ?? null,
|
|
365
|
+
company_entity_id: profile.company && typeof profile.company === "object" ? profile.company.id : profile.company ?? null
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
227
369
|
}
|
|
228
370
|
});
|
|
229
371
|
const { POST, PUT, DELETE } = crud;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/api/people/route.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CustomerEntity } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { personCreateSchema, personUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../utils'\nimport { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n enrichers: { entityId: 'customers.person' },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any, ctx) => {\n const filters: Record<string, any> = { kind: { $eq: 'person' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n filters.display_name = { $ilike: `%${escapeLikePattern(query.search)}%` }\n }\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }\n } else if (emailContains) {\n filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as any\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_person_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore custom field filter errors; fall back to base filters\n }\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_person_profile,\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item: any) => {\n if (!item) return item\n const normalized = { ...item }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.people.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n personId: result?.personId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.people.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.people.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.person_required', 'Person id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst personListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst personCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n personId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Person',\n pluralName: 'People',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(personListItemSchema),\n create: {\n schema: personCreateSchema,\n responseSchema: personCreateResponseSchema,\n description: 'Creates a person contact using scoped organization and tenant identifiers.',\n },\n update: {\n schema: personUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates contact details or custom fields for a person.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a person by id. Request body or query may provide the identifier.',\n },\n})\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAClB,SAAS,oBAAoB,0BAA0B;AACvD,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;AAClC,SAAS,kCAAkC,8BAA8B,+BAA+B;AACxG,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,aAAa,EAChB,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EACnC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,EACpC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC1C,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,SAAS;AACnC,CAAC,EACA,YAAY;AAEf,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AAAA,EACrE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,yBAAyB,EAAE;AAAA,EACxE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,yBAAyB,EAAE;AAAA,EACvE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,yBAAyB,EAAE;AAC5E;AAEO,MAAM,WAAW;AAExB,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,WAAW,EAAE,UAAU,mBAAmB;AAAA,EAC1C,MAAM;AAAA,IACJ,QAAQ;AAAA,IACR,UAAU,EAAE,UAAU;AAAA,IACtB,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,IACA,cAAc,OAAO,OAAY,QAAQ;AACvC,YAAM,UAA+B,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE;AAC/D,UAAI,MAAM,GAAI,SAAQ,KAAK,EAAE,KAAK,MAAM,GAAG;AAC3C,UAAI,MAAM,QAAQ;AAChB,gBAAQ,eAAe,EAAE,QAAQ,IAAI,kBAAkB,MAAM,MAAM,CAAC,IAAI;AAAA,MAC1E;AACA,YAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,MAAM,MAAM,KAAK,EAAE,YAAY,IAAI;AACnF,YAAM,kBAAkB,OAAO,MAAM,oBAAoB,WAAW,MAAM,gBAAgB,KAAK,EAAE,YAAY,IAAI;AACjH,YAAM,gBAAgB,OAAO,MAAM,kBAAkB,WAAW,MAAM,cAAc,KAAK,EAAE,YAAY,IAAI;AAC3G,UAAI,OAAO;AACT,gBAAQ,gBAAgB,EAAE,KAAK,MAAM;AAAA,MACvC,WAAW,iBAAiB;AAC1B,gBAAQ,gBAAgB,EAAE,QAAQ,GAAG,kBAAkB,eAAe,CAAC,IAAI;AAAA,MAC7E,WAAW,eAAe;AACxB,gBAAQ,gBAAgB,EAAE,QAAQ,IAAI,kBAAkB,aAAa,CAAC,IAAI;AAAA,MAC5E;AACA,UAAI,MAAM,QAAQ;AAChB,gBAAQ,SAAS,EAAE,KAAK,MAAM,OAAO;AAAA,MACvC;AACA,UAAI,MAAM,gBAAgB;AACxB,gBAAQ,kBAAkB,EAAE,KAAK,MAAM,eAAe;AAAA,MACxD;AACA,UAAI,MAAM,QAAQ;AAChB,gBAAQ,SAAS,EAAE,KAAK,MAAM,OAAO;AAAA,MACvC;AACA,YAAM,YAAY,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;AACpE,YAAM,SAAS,UACZ,MAAM,GAAG,EACT,IAAI,CAAC,UAAkB,MAAM,KAAK,CAAC,EACnC,OAAO,CAAC,UAAkB,MAAM,SAAS,CAAC;AAC7C,YAAM,cAAc,kBAAkB,MAAM,WAAW,MAAM;AAC7D,UAAI,aAAa;AACf,gBAAQ,KAAK,EAAE,KAAK,uCAAuC;AAAA,MAC7D,WAAW,OAAO,SAAS,GAAG;AAC5B,gBAAQ,wBAAwB,IAAI,EAAE,KAAK,OAAO;AAAA,MACpD;AACA,YAAM,WAAW,kBAAkB,MAAM,QAAQ;AACjD,UAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,iBAAiB,aAAa,MAAM;AACrE,gBAAQ,gBAAgB,EAAE,SAAS,SAAS;AAAA,MAC9C;AACA,YAAM,WAAW,kBAAkB,MAAM,QAAQ;AACjD,UAAI,aAAa,MAAM;AACrB,gBAAQ,gBAAgB,EAAE,SAAS,SAAS;AAAA,MAC9C;AACA,YAAM,qBAAqB,kBAAkB,MAAM,kBAAkB;AACrE,UAAI,uBAAuB,MAAM;AAC/B,gBAAQ,sBAAsB,EAAE,SAAS,mBAAmB;AAAA,MAC9D;AACA,YAAM,eAAqC,CAAC;AAC5C,UAAI,MAAM,aAAa;AACrB,cAAM,OAAO,IAAI,KAAK,MAAM,WAAW;AACvC,YAAI,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,cAAa,OAAO;AAAA,MACzD;AACA,UAAI,MAAM,WAAW;AACnB,cAAM,KAAK,IAAI,KAAK,MAAM,SAAS;AACnC,YAAI,CAAC,OAAO,MAAM,GAAG,QAAQ,CAAC,EAAG,cAAa,OAAO;AAAA,MACvD;AACA,UAAI,OAAO,KAAK,YAAY,EAAE,QAAQ;AACpC,gBAAQ,aAAa;AAAA,MACvB;AACA,UAAI,KAAK;AACP,YAAI;AACF,gBAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,gBAAM,YAAY,MAAM,iCAAiC;AAAA,YACvD,WAAW,CAAC,EAAE,UAAU,iBAAiB,EAAE,UAAU,uBAAuB;AAAA,YAC5E;AAAA,YACA;AAAA,YACA,UAAU,IAAI,MAAM,YAAY;AAAA,UAClC,CAAC;AACD,iBAAO,OAAO,SAAS,SAAS;AAAA,QAClC,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,oBAAoB;AAAA,MAClB;AAAA,QACE,UAAU,EAAE,UAAU;AAAA,QACtB,OAAO;AAAA,QACP,OAAO;AAAA,QACP,gBAAgB;AAAA,QAChB,MAAM,EAAE,WAAW,MAAM,SAAS,YAAY;AAAA,MAChD;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL;AAAA,QACE,OAAO;AAAA,QACP,OAAO;AAAA,QACP,MAAM,EAAE,OAAO,KAAK;AAAA,QACpB,IAAI,EAAE,OAAO,YAAY;AAAA,QACzB,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,eAAe,CAAC,SAAc;AAC5B,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,aAAa,EAAE,GAAG,KAAK;AAC7B,aAAO,WAAW;AAClB,YAAM,YAAY,6BAA6B,IAAI;AACnD,iBAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AACzC,YAAI,IAAI,WAAW,KAAK,GAAG;AACzB,iBAAO,WAAW,GAAG;AAAA,QACvB;AAAA,MACF;AACA,aAAO,EAAE,GAAG,YAAY,GAAG,UAAU;AAAA,IACvC;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,cAAM,EAAE,MAAM,OAAO,IAAI,wBAAwB,MAAM;AACvD,cAAM,SAAS,mBAAmB,MAAM,IAAI;AAC5C,eAAO,OAAO,KAAK,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,cAAc,OAAO,IAAI;AAAA,MAC5E;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,IAAI,QAAQ,YAAY,QAAQ,MAAM;AAAA,QACtC,UAAU,QAAQ,YAAY;AAAA,MAChC;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,cAAM,EAAE,MAAM,OAAO,IAAI,wBAAwB,MAAM;AACvD,cAAM,SAAS,mBAAmB,MAAM,IAAI;AAC5C,eAAO,OAAO,KAAK,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,cAAc,OAAO,IAAI;AAAA,MAC5E;AAAA,MACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,KACJ,QAAQ,MAAM,MACd,QAAQ,MACR,QAAQ,OAAO,OACd,IAAI,UAAU,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,IAAI,IAAI;AACnE,YAAI,CAAC,GAAI,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,oCAAoC,uBAAuB,EAAE,CAAC;AACvH,eAAO,EAAE,GAAG;AAAA,MACd;AAAA,MACA,UAAU,OAAO,EAAE,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AACF,CAAC;AAED,MAAM,EAAE,MAAM,KAAK,OAAO,IAAI;AAGvB,MAAM,MAAM,KAAK;AAExB,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC5C,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACrD,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAChD,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACpD,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,yBAAyB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACxD,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACtD,wBAAwB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACvD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;AAAA,EACjD,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC7C,CAAC;AAED,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,UAAU,2BAA2B;AAAA,EAChD,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB,8BAA8B,oBAAoB;AAAA,EACtE,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AAAA,EACA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AAAA,EACA,KAAK;AAAA,IACH,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,IAC1C,gBAAgB;AAAA,IAChB,aAAa;AAAA,EACf;AACF,CAAC;",
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CustomerEntity, CustomerPersonProfile } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { personCreateSchema, personUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n applyEntityIdRestriction,\n consumeAdvancedFilterState,\n findMatchingEntityIdsWithQueryEngine,\n findMatchingEntityIdsBySearchTokensAcrossSources,\n withScopedPayload,\n} from '../utils'\nimport { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { mergeAdvancedFilters } from '@open-mercato/shared/lib/crud/advanced-filter-integration'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n enrichers: { entityId: 'customers.person' },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any, ctx) => {\n const advancedQuery = { ...query }\n const advancedFilterState = consumeAdvancedFilterState(query)\n const filters: Record<string, any> = { kind: { $eq: 'person' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n const matchingIds = ctx\n ? await findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query: query.search,\n sources: [\n {\n entityType: E.customers.customer_entity,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'description',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_name',\n ],\n },\n {\n entityType: E.customers.customer_person_profile,\n fields: [\n 'display_name',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'first_name',\n 'last_name',\n 'preferred_name',\n 'job_title',\n 'department',\n 'seniority',\n 'timezone',\n 'linked_in_url',\n 'twitter_url',\n ],\n mapToEntityIds: {\n table: 'customer_people',\n targetColumn: 'entity_id',\n },\n },\n ],\n })\n : null\n if (matchingIds !== null && matchingIds.length > 0) {\n applyEntityIdRestriction(filters, matchingIds)\n } else {\n const searchPattern = `%${escapeLikePattern(query.search)}%`\n filters.$or = [\n { display_name: { $ilike: searchPattern } },\n { primary_email: { $ilike: searchPattern } },\n { primary_phone: { $ilike: searchPattern } },\n { description: { $ilike: searchPattern } },\n { next_interaction_name: { $ilike: searchPattern } },\n ]\n }\n }\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }\n } else if (emailContains) {\n filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as any\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_person_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore custom field filter errors; fall back to base filters\n }\n }\n if (ctx && advancedFilterState) {\n const advancedFilters = mergeAdvancedFilters(\n { ...filters },\n advancedQuery as Record<string, unknown>,\n )\n const matchedIds = await findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId: E.customers.customer_entity,\n filters: advancedFilters,\n customFieldSources: [\n {\n entityId: E.customers.customer_person_profile,\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n })\n applyEntityIdRestriction(filters, matchedIds)\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_person_profile,\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item: any) => {\n if (!item) return item\n const normalized = { ...item }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.people.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n personId: result?.personId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.people.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.people.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.person_required', 'Person id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n const items = Array.isArray(payload?.items) ? payload.items : []\n const ids = items\n .map((item: unknown) => (\n item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'\n ? (item as Record<string, unknown>).id as string\n : null\n ))\n .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)\n if (!ids.length) return\n\n const where: Record<string, unknown> = {\n entity: { $in: ids },\n tenantId: ctx.auth?.tenantId ?? null,\n }\n if (ctx.selectedOrganizationId) {\n where.organizationId = ctx.selectedOrganizationId\n }\n\n const profiles = await findWithDecryption(\n ctx.container.resolve('em') as any,\n CustomerPersonProfile,\n where as any,\n { populate: ['entity', 'company'] } as any,\n {\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n },\n )\n\n const profilesByEntityId = new Map<string, CustomerPersonProfile>()\n for (const profile of profiles) {\n const entityId = typeof (profile as any)?.entity?.id === 'string' ? (profile as any).entity.id : null\n if (entityId) profilesByEntityId.set(entityId, profile)\n }\n\n payload.items = items.map((item: unknown) => {\n if (!item || typeof item !== 'object') return item\n const record = item as Record<string, unknown>\n const profile = typeof record.id === 'string' ? profilesByEntityId.get(record.id) : undefined\n if (!profile) return item\n return {\n ...record,\n first_name: profile.firstName ?? null,\n last_name: profile.lastName ?? null,\n preferred_name: profile.preferredName ?? null,\n job_title: profile.jobTitle ?? null,\n department: profile.department ?? null,\n seniority: profile.seniority ?? null,\n timezone: profile.timezone ?? null,\n linked_in_url: profile.linkedInUrl ?? null,\n twitter_url: profile.twitterUrl ?? null,\n company_entity_id:\n profile.company && typeof profile.company === 'object'\n ? profile.company.id\n : profile.company ?? null,\n }\n })\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst personListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst personCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n personId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Person',\n pluralName: 'People',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(personListItemSchema),\n create: {\n schema: personCreateSchema,\n responseSchema: personCreateResponseSchema,\n description: 'Creates a person contact using scoped organization and tenant identifiers.',\n },\n update: {\n schema: personUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates contact details or custom fields for a person.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a person by id. Request body or query may provide the identifier.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,206 @@
|
|
|
1
1
|
import { createScopedApiHelpers } from "@open-mercato/shared/lib/api/scoped";
|
|
2
|
+
import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
|
|
3
|
+
import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
|
|
4
|
+
import { deserializeAdvancedFilter } from "@open-mercato/shared/lib/query/advanced-filter";
|
|
5
|
+
import { SortDir } from "@open-mercato/shared/lib/query/types";
|
|
2
6
|
const { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({
|
|
3
7
|
messages: {
|
|
4
8
|
tenantRequired: { key: "customers.errors.tenant_required", fallback: "Tenant context is required" },
|
|
5
9
|
organizationRequired: { key: "customers.errors.organization_required", fallback: "Organization context is required" }
|
|
6
10
|
}
|
|
7
11
|
});
|
|
12
|
+
const NO_MATCH_ID = "00000000-0000-0000-0000-000000000000";
|
|
13
|
+
async function enrichSearchSourcesWithCustomFieldTokens(ctx, sources) {
|
|
14
|
+
const entityTypes = Array.from(
|
|
15
|
+
new Set(
|
|
16
|
+
sources.map((source) => source.entityType).filter((value) => typeof value === "string" && value.length > 0)
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
if (!entityTypes.length) return sources;
|
|
20
|
+
const em = ctx.container.resolve("em");
|
|
21
|
+
const knex = em.getConnection().getKnex();
|
|
22
|
+
let defsQuery = knex("custom_field_defs").select("entity_id", "key", "kind").whereIn("entity_id", entityTypes).andWhere("is_active", true);
|
|
23
|
+
defsQuery = defsQuery.andWhere((builder) => {
|
|
24
|
+
builder.where({ tenant_id: ctx.auth?.tenantId ?? null }).orWhereNull("tenant_id");
|
|
25
|
+
});
|
|
26
|
+
if (ctx.selectedOrganizationId) {
|
|
27
|
+
defsQuery = defsQuery.andWhere((builder) => {
|
|
28
|
+
builder.where({ organization_id: ctx.selectedOrganizationId }).orWhereNull("organization_id");
|
|
29
|
+
});
|
|
30
|
+
} else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
|
|
31
|
+
defsQuery = defsQuery.andWhere((builder) => {
|
|
32
|
+
builder.whereIn("organization_id", ctx.organizationIds).orWhereNull("organization_id");
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const customFieldKeysByEntity = /* @__PURE__ */ new Map();
|
|
36
|
+
const rows = await defsQuery;
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
if (row.kind === "attachment") continue;
|
|
39
|
+
const entityType = typeof row.entity_id === "string" ? row.entity_id : null;
|
|
40
|
+
const key = typeof row.key === "string" ? row.key.trim() : "";
|
|
41
|
+
if (!entityType || !key) continue;
|
|
42
|
+
const bucket = customFieldKeysByEntity.get(entityType) ?? /* @__PURE__ */ new Set();
|
|
43
|
+
bucket.add(`cf:${key}`);
|
|
44
|
+
customFieldKeysByEntity.set(entityType, bucket);
|
|
45
|
+
}
|
|
46
|
+
return sources.map((source) => {
|
|
47
|
+
const customFieldKeys = customFieldKeysByEntity.get(source.entityType);
|
|
48
|
+
return {
|
|
49
|
+
...source,
|
|
50
|
+
fields: Array.from(/* @__PURE__ */ new Set([
|
|
51
|
+
"search_text",
|
|
52
|
+
...source.fields,
|
|
53
|
+
...customFieldKeys ? Array.from(customFieldKeys) : []
|
|
54
|
+
]))
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function findSearchTokenEntityIds({
|
|
59
|
+
ctx,
|
|
60
|
+
entityType,
|
|
61
|
+
fields,
|
|
62
|
+
query
|
|
63
|
+
}) {
|
|
64
|
+
const trimmed = query.trim();
|
|
65
|
+
if (!trimmed) return null;
|
|
66
|
+
const tokens = tokenizeText(trimmed, resolveSearchConfig());
|
|
67
|
+
if (!tokens.hashes.length) return [];
|
|
68
|
+
const em = ctx.container.resolve("em");
|
|
69
|
+
const knex = em.getConnection().getKnex();
|
|
70
|
+
let searchQuery = knex("search_tokens").select("entity_id").where("entity_type", entityType).whereIn("field", fields).whereIn("token_hash", tokens.hashes).groupBy("entity_id").havingRaw("count(distinct token_hash) >= ?", [tokens.hashes.length]);
|
|
71
|
+
if (ctx.auth?.tenantId !== void 0) {
|
|
72
|
+
searchQuery = searchQuery.whereRaw("tenant_id is not distinct from ?", [ctx.auth?.tenantId ?? null]);
|
|
73
|
+
}
|
|
74
|
+
if (ctx.selectedOrganizationId) {
|
|
75
|
+
searchQuery = searchQuery.where("organization_id", ctx.selectedOrganizationId);
|
|
76
|
+
} else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
|
|
77
|
+
searchQuery = searchQuery.whereIn("organization_id", ctx.organizationIds);
|
|
78
|
+
}
|
|
79
|
+
const rows = await searchQuery;
|
|
80
|
+
return rows.map((row) => typeof row.entity_id === "string" ? row.entity_id : null).filter((id) => typeof id === "string" && id.length > 0);
|
|
81
|
+
}
|
|
82
|
+
async function mapScopedEntityIds({
|
|
83
|
+
ctx,
|
|
84
|
+
ids,
|
|
85
|
+
config
|
|
86
|
+
}) {
|
|
87
|
+
if (!ids.length) return [];
|
|
88
|
+
const em = ctx.container.resolve("em");
|
|
89
|
+
const knex = em.getConnection().getKnex();
|
|
90
|
+
const sourceColumn = config.sourceColumn ?? "id";
|
|
91
|
+
const tenantColumn = config.tenantColumn ?? "tenant_id";
|
|
92
|
+
const organizationColumn = config.organizationColumn ?? "organization_id";
|
|
93
|
+
let mapQuery = knex(config.table).select(config.targetColumn).whereIn(sourceColumn, ids);
|
|
94
|
+
if (ctx.auth?.tenantId !== void 0) {
|
|
95
|
+
mapQuery = mapQuery.whereRaw("?? is not distinct from ?", [tenantColumn, ctx.auth?.tenantId ?? null]);
|
|
96
|
+
}
|
|
97
|
+
if (ctx.selectedOrganizationId) {
|
|
98
|
+
mapQuery = mapQuery.where(organizationColumn, ctx.selectedOrganizationId);
|
|
99
|
+
} else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
|
|
100
|
+
mapQuery = mapQuery.whereIn(organizationColumn, ctx.organizationIds);
|
|
101
|
+
}
|
|
102
|
+
const rows = await mapQuery;
|
|
103
|
+
return rows.map((row) => {
|
|
104
|
+
const value = row[config.targetColumn];
|
|
105
|
+
return typeof value === "string" ? value : null;
|
|
106
|
+
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
107
|
+
}
|
|
108
|
+
async function findMatchingEntityIdsBySearchTokensAcrossSources({
|
|
109
|
+
ctx,
|
|
110
|
+
sources,
|
|
111
|
+
query
|
|
112
|
+
}) {
|
|
113
|
+
const trimmed = query.trim();
|
|
114
|
+
if (!trimmed) return null;
|
|
115
|
+
const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources);
|
|
116
|
+
const matchedIds = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const source of enrichedSources) {
|
|
118
|
+
const rawIds = await findSearchTokenEntityIds({
|
|
119
|
+
ctx,
|
|
120
|
+
entityType: source.entityType,
|
|
121
|
+
fields: source.fields,
|
|
122
|
+
query: trimmed
|
|
123
|
+
});
|
|
124
|
+
if (rawIds === null) return null;
|
|
125
|
+
const entityIds = source.mapToEntityIds ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds }) : rawIds;
|
|
126
|
+
entityIds.forEach((id) => matchedIds.add(id));
|
|
127
|
+
}
|
|
128
|
+
return Array.from(matchedIds);
|
|
129
|
+
}
|
|
130
|
+
async function findMatchingEntityIdsBySearchTokens({
|
|
131
|
+
ctx,
|
|
132
|
+
entityType,
|
|
133
|
+
fields,
|
|
134
|
+
query
|
|
135
|
+
}) {
|
|
136
|
+
return findMatchingEntityIdsBySearchTokensAcrossSources({
|
|
137
|
+
ctx,
|
|
138
|
+
query,
|
|
139
|
+
sources: [{ entityType, fields }]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function applyEntityIdRestriction(filters, ids) {
|
|
143
|
+
if (ids === null) return;
|
|
144
|
+
const currentIdFilter = filters.id && typeof filters.id === "object" && !Array.isArray(filters.id) ? filters.id : null;
|
|
145
|
+
const currentEq = typeof currentIdFilter?.$eq === "string" ? currentIdFilter.$eq : null;
|
|
146
|
+
if (currentEq) {
|
|
147
|
+
filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID };
|
|
151
|
+
}
|
|
152
|
+
function consumeAdvancedFilterState(query) {
|
|
153
|
+
const state = deserializeAdvancedFilter(query);
|
|
154
|
+
if (!state) return null;
|
|
155
|
+
for (const key of Object.keys(query)) {
|
|
156
|
+
if (key.startsWith("filter[")) {
|
|
157
|
+
delete query[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
async function findMatchingEntityIdsWithQueryEngine({
|
|
163
|
+
ctx,
|
|
164
|
+
entityId,
|
|
165
|
+
filters,
|
|
166
|
+
customFieldSources,
|
|
167
|
+
joins
|
|
168
|
+
}) {
|
|
169
|
+
const qe = ctx.container.resolve("queryEngine");
|
|
170
|
+
const ids = /* @__PURE__ */ new Set();
|
|
171
|
+
const pageSize = 100;
|
|
172
|
+
let page = 1;
|
|
173
|
+
let total = 0;
|
|
174
|
+
do {
|
|
175
|
+
const result = await qe.query(entityId, {
|
|
176
|
+
fields: ["id"],
|
|
177
|
+
filters,
|
|
178
|
+
page: { page, pageSize },
|
|
179
|
+
sort: [{ field: "id", dir: SortDir.Asc }],
|
|
180
|
+
tenantId: ctx.auth?.tenantId ?? void 0,
|
|
181
|
+
organizationId: ctx.selectedOrganizationId ?? void 0,
|
|
182
|
+
organizationIds: ctx.organizationIds ?? void 0,
|
|
183
|
+
customFieldSources,
|
|
184
|
+
joins
|
|
185
|
+
});
|
|
186
|
+
total = result.total ?? 0;
|
|
187
|
+
for (const item of result.items ?? []) {
|
|
188
|
+
const id = item && typeof item === "object" ? item.id : null;
|
|
189
|
+
if (typeof id === "string" && id.length > 0) {
|
|
190
|
+
ids.add(id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!result.items?.length) break;
|
|
194
|
+
page += 1;
|
|
195
|
+
} while (ids.size < total);
|
|
196
|
+
return Array.from(ids);
|
|
197
|
+
}
|
|
8
198
|
export {
|
|
199
|
+
applyEntityIdRestriction,
|
|
200
|
+
consumeAdvancedFilterState,
|
|
201
|
+
findMatchingEntityIdsBySearchTokens,
|
|
202
|
+
findMatchingEntityIdsBySearchTokensAcrossSources,
|
|
203
|
+
findMatchingEntityIdsWithQueryEngine,
|
|
9
204
|
parseScopedCommandInput,
|
|
10
205
|
withScopedPayload
|
|
11
206
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/api/utils.ts"],
|
|
4
|
-
"sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { deserializeAdvancedFilter } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { SortDir } from '@open-mercato/shared/lib/query/types'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\ntype SearchTokenMatchInput = {\n ctx: CrudCtx\n entityType: string\n fields: string[]\n query: string\n}\n\ntype SearchTokenSource = {\n entityType: string\n fields: string[]\n mapToEntityIds?: {\n table: string\n sourceColumn?: string\n targetColumn: string\n tenantColumn?: string\n organizationColumn?: string\n }\n}\n\nasync function enrichSearchSourcesWithCustomFieldTokens(\n ctx: CrudCtx,\n sources: SearchTokenSource[],\n): Promise<SearchTokenSource[]> {\n const entityTypes = Array.from(\n new Set(\n sources\n .map((source) => source.entityType)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n if (!entityTypes.length) return sources\n\n const em = ctx.container.resolve('em') as EntityManager\n const knex = (em as any).getConnection().getKnex()\n let defsQuery = knex('custom_field_defs')\n .select('entity_id', 'key', 'kind')\n .whereIn('entity_id', entityTypes)\n .andWhere('is_active', true)\n\n defsQuery = defsQuery.andWhere((builder: any) => {\n builder.where({ tenant_id: ctx.auth?.tenantId ?? null }).orWhereNull('tenant_id')\n })\n\n if (ctx.selectedOrganizationId) {\n defsQuery = defsQuery.andWhere((builder: any) => {\n builder.where({ organization_id: ctx.selectedOrganizationId }).orWhereNull('organization_id')\n })\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n defsQuery = defsQuery.andWhere((builder: any) => {\n builder.whereIn('organization_id', ctx.organizationIds).orWhereNull('organization_id')\n })\n }\n\n const customFieldKeysByEntity = new Map<string, Set<string>>()\n const rows = await defsQuery\n for (const row of rows as Array<{ entity_id?: unknown; key?: unknown; kind?: unknown }>) {\n if (row.kind === 'attachment') continue\n const entityType = typeof row.entity_id === 'string' ? row.entity_id : null\n const key = typeof row.key === 'string' ? row.key.trim() : ''\n if (!entityType || !key) continue\n const bucket = customFieldKeysByEntity.get(entityType) ?? new Set<string>()\n bucket.add(`cf:${key}`)\n customFieldKeysByEntity.set(entityType, bucket)\n }\n\n return sources.map((source) => {\n const customFieldKeys = customFieldKeysByEntity.get(source.entityType)\n return {\n ...source,\n fields: Array.from(new Set([\n 'search_text',\n ...source.fields,\n ...(customFieldKeys ? Array.from(customFieldKeys) : []),\n ])),\n }\n })\n}\n\nasync function findSearchTokenEntityIds({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const tokens = tokenizeText(trimmed, resolveSearchConfig())\n if (!tokens.hashes.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const knex = (em as any).getConnection().getKnex()\n let searchQuery = knex('search_tokens')\n .select('entity_id')\n .where('entity_type', entityType)\n .whereIn('field', fields)\n .whereIn('token_hash', tokens.hashes)\n .groupBy('entity_id')\n .havingRaw('count(distinct token_hash) >= ?', [tokens.hashes.length])\n\n if (ctx.auth?.tenantId !== undefined) {\n searchQuery = searchQuery.whereRaw('tenant_id is not distinct from ?', [ctx.auth?.tenantId ?? null])\n }\n if (ctx.selectedOrganizationId) {\n searchQuery = searchQuery.where('organization_id', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n searchQuery = searchQuery.whereIn('organization_id', ctx.organizationIds)\n }\n\n const rows = await searchQuery\n return rows\n .map((row: { entity_id?: unknown }) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)\n}\n\nasync function mapScopedEntityIds({\n ctx,\n ids,\n config,\n}: {\n ctx: CrudCtx\n ids: string[]\n config: NonNullable<SearchTokenSource['mapToEntityIds']>\n}): Promise<string[]> {\n if (!ids.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const knex = (em as any).getConnection().getKnex()\n const sourceColumn = config.sourceColumn ?? 'id'\n const tenantColumn = config.tenantColumn ?? 'tenant_id'\n const organizationColumn = config.organizationColumn ?? 'organization_id'\n\n let mapQuery = knex(config.table)\n .select(config.targetColumn)\n .whereIn(sourceColumn, ids)\n\n if (ctx.auth?.tenantId !== undefined) {\n mapQuery = mapQuery.whereRaw('?? is not distinct from ?', [tenantColumn, ctx.auth?.tenantId ?? null])\n }\n if (ctx.selectedOrganizationId) {\n mapQuery = mapQuery.where(organizationColumn, ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n mapQuery = mapQuery.whereIn(organizationColumn, ctx.organizationIds)\n }\n\n const rows = await mapQuery\n return rows\n .map((row: Record<string, unknown>) => {\n const value = row[config.targetColumn]\n return typeof value === 'string' ? value : null\n })\n .filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)\n}\n\nexport async function findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n sources,\n query,\n}: {\n ctx: CrudCtx\n sources: SearchTokenSource[]\n query: string\n}): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)\n const matchedIds = new Set<string>()\n for (const source of enrichedSources) {\n const rawIds = await findSearchTokenEntityIds({\n ctx,\n entityType: source.entityType,\n fields: source.fields,\n query: trimmed,\n })\n if (rawIds === null) return null\n const entityIds = source.mapToEntityIds\n ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })\n : rawIds\n entityIds.forEach((id) => matchedIds.add(id))\n }\n\n return Array.from(matchedIds)\n}\n\nexport async function findMatchingEntityIdsBySearchTokens({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n return findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query,\n sources: [{ entityType, fields }],\n })\n}\n\nexport function applyEntityIdRestriction(\n filters: Record<string, unknown>,\n ids: string[] | null,\n): void {\n if (ids === null) return\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n\n if (currentEq) {\n filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID }\n}\n\nexport function consumeAdvancedFilterState(query: Record<string, unknown>) {\n const state = deserializeAdvancedFilter(query)\n if (!state) return null\n\n for (const key of Object.keys(query)) {\n if (key.startsWith('filter[')) {\n delete query[key]\n }\n }\n\n return state\n}\n\nexport async function findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId,\n filters,\n customFieldSources,\n joins,\n}: {\n ctx: CrudCtx\n entityId: EntityId\n filters: Record<string, unknown>\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n}): Promise<string[]> {\n const qe = ctx.container.resolve('queryEngine') as QueryEngine\n const ids = new Set<string>()\n const pageSize = 100\n let page = 1\n let total = 0\n\n do {\n const result = await qe.query(entityId, {\n fields: ['id'],\n filters,\n page: { page, pageSize },\n sort: [{ field: 'id', dir: SortDir.Asc }],\n tenantId: ctx.auth?.tenantId ?? undefined,\n organizationId: ctx.selectedOrganizationId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n customFieldSources,\n joins,\n })\n\n total = result.total ?? 0\n for (const item of result.items ?? []) {\n const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null\n if (typeof id === 'string' && id.length > 0) {\n ids.add(id)\n }\n }\n if (!result.items?.length) break\n page += 1\n } while (ids.size < total)\n\n return Array.from(ids)\n}\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|