@open-mercato/core 0.4.11-develop.1416.adda6008da → 0.4.11-develop.1418.27a299bdaf

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.
@@ -1,6 +1,221 @@
1
+ import { runWithCacheTenant } from "@open-mercato/cache";
1
2
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
3
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
3
4
  import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from "../notifications/lib/deliveryConfig.js";
5
+ import { Tenant } from "../directory/data/entities.js";
6
+ import {
7
+ collectCacheStats,
8
+ executeCachePurge,
9
+ previewCachePurge
10
+ } from "./lib/cache-cli.js";
11
+ function parseArgs(rest) {
12
+ const args = {};
13
+ for (let i = 0; i < rest.length; i += 1) {
14
+ const part = rest[i];
15
+ if (!part?.startsWith("--")) continue;
16
+ const [rawKey, rawValue] = part.slice(2).split("=");
17
+ if (!rawKey) continue;
18
+ if (rawValue !== void 0) {
19
+ args[rawKey] = rawValue;
20
+ } else if (i + 1 < rest.length && !rest[i + 1].startsWith("--")) {
21
+ args[rawKey] = rest[i + 1];
22
+ i += 1;
23
+ } else {
24
+ args[rawKey] = true;
25
+ }
26
+ }
27
+ return args;
28
+ }
29
+ function stringOption(args, ...keys) {
30
+ for (const key of keys) {
31
+ const raw = args[key];
32
+ if (typeof raw !== "string") continue;
33
+ const trimmed = raw.trim();
34
+ if (trimmed.length > 0) return trimmed;
35
+ }
36
+ return void 0;
37
+ }
38
+ function flagEnabled(args, ...keys) {
39
+ for (const key of keys) {
40
+ const raw = args[key];
41
+ if (raw === void 0) continue;
42
+ if (raw === true) return true;
43
+ if (typeof raw === "string") {
44
+ const parsed = parseBooleanToken(raw);
45
+ return parsed === null ? true : parsed;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ function splitListOption(raw) {
51
+ if (!raw) return [];
52
+ const seen = /* @__PURE__ */ new Set();
53
+ const values = [];
54
+ for (const item of raw.split(",")) {
55
+ const trimmed = item.trim();
56
+ if (!trimmed || seen.has(trimmed)) continue;
57
+ seen.add(trimmed);
58
+ values.push(trimmed);
59
+ }
60
+ return values;
61
+ }
62
+ async function resolveCacheScopes(em, args) {
63
+ const explicitTenantId = stringOption(args, "tenant", "tenantId");
64
+ const globalOnly = flagEnabled(args, "global");
65
+ const allTenants = flagEnabled(args, "all-tenants", "allTenants");
66
+ if (explicitTenantId && globalOnly) {
67
+ throw new Error("Cannot combine `--tenant` with `--global`.");
68
+ }
69
+ if (explicitTenantId && allTenants) {
70
+ throw new Error("Cannot combine `--tenant` with `--all-tenants`.");
71
+ }
72
+ if (globalOnly && allTenants) {
73
+ throw new Error("Cannot combine `--global` with `--all-tenants`.");
74
+ }
75
+ if (explicitTenantId) {
76
+ return [{ label: `tenant:${explicitTenantId}`, tenantId: explicitTenantId }];
77
+ }
78
+ if (globalOnly) {
79
+ return [{ label: "global", tenantId: null }];
80
+ }
81
+ if (!allTenants) {
82
+ return [{ label: "global", tenantId: null }];
83
+ }
84
+ const tenants = await em.find(Tenant, { deletedAt: null }, { orderBy: { name: "asc" } });
85
+ const scopes = [{ label: "global", tenantId: null }];
86
+ const seen = /* @__PURE__ */ new Set();
87
+ for (const tenant of tenants) {
88
+ const tenantId = typeof tenant.id === "string" ? tenant.id : "";
89
+ if (!tenantId || seen.has(tenantId)) continue;
90
+ seen.add(tenantId);
91
+ scopes.push({ label: `tenant:${tenantId}`, tenantId });
92
+ }
93
+ return scopes;
94
+ }
95
+ function resolveCachePurgeRequest(args) {
96
+ if (flagEnabled(args, "all")) return { kind: "all" };
97
+ const segment = stringOption(args, "segment");
98
+ if (segment) return { kind: "segment", segment };
99
+ const tags = splitListOption(stringOption(args, "tag", "tags"));
100
+ if (tags.length > 0) return { kind: "tags", tags };
101
+ const keys = splitListOption(stringOption(args, "key", "keys"));
102
+ if (keys.length > 0) return { kind: "keys", keys };
103
+ const ids = splitListOption(stringOption(args, "id", "ids"));
104
+ if (ids.length > 0) return { kind: "ids", ids };
105
+ const pattern = stringOption(args, "pattern");
106
+ if (pattern) return { kind: "pattern", pattern };
107
+ throw new Error(
108
+ "Choose a purge target: `--all`, `--segment <id>`, `--tag <tag1,tag2>`, `--key <key1,key2>`, `--id <token1,token2>`, or `--pattern <glob>`."
109
+ );
110
+ }
111
+ function printCacheHelp() {
112
+ console.log("\u{1F9F9} Cache CLI");
113
+ console.log("");
114
+ console.log("\u{1F680} Usage:");
115
+ console.log(" yarn mercato configs cache stats [--tenant <id> | --global | --all-tenants] [--json]");
116
+ console.log(" yarn mercato configs cache purge --all [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
117
+ console.log(" yarn mercato configs cache purge --segment <segment> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
118
+ console.log(" yarn mercato configs cache purge --tag <tag1,tag2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
119
+ console.log(" yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
120
+ console.log(" yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
121
+ console.log(" yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
122
+ console.log(" yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
123
+ console.log("");
124
+ console.log("\u2139\uFE0F Notes:");
125
+ console.log(" `stats` mirrors the cache admin page segment overview for CRUD/widget caches.");
126
+ console.log(" `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).");
127
+ console.log(" `structural` targets navigation caches (`nav:*`) and is the recommended post-step after module/sidebar structure changes.");
128
+ console.log(" When no scope flag is supplied, this command uses the global cache scope only.");
129
+ }
130
+ async function disposeContainer(container) {
131
+ const disposable = container;
132
+ if (typeof disposable.dispose === "function") {
133
+ await disposable.dispose();
134
+ }
135
+ }
136
+ async function runCacheStats(args) {
137
+ const json = flagEnabled(args, "json");
138
+ const container = await createRequestContainer();
139
+ try {
140
+ const em = container.resolve("em");
141
+ const cache = container.resolve("cache");
142
+ const scopes = await resolveCacheScopes(em, args);
143
+ const results = [];
144
+ for (const scope of scopes) {
145
+ const stats = await runWithCacheTenant(scope.tenantId, async () => collectCacheStats(cache));
146
+ results.push({ scope: scope.label, ...stats });
147
+ }
148
+ if (json) {
149
+ console.log(JSON.stringify(results, null, 2));
150
+ return;
151
+ }
152
+ for (const result of results) {
153
+ console.log(`\u{1F50E} [cache] scope=${result.scope} totalKeys=${result.totalKeys} generatedAt=${result.generatedAt}`);
154
+ if (result.segments.length === 0) {
155
+ console.log(" \u2205 segments: none");
156
+ continue;
157
+ }
158
+ for (const segment of result.segments) {
159
+ console.log(` \u2022 ${segment.segment} (${segment.keyCount})${segment.path ? ` ${segment.path}` : ""}`);
160
+ }
161
+ }
162
+ } finally {
163
+ await disposeContainer(container);
164
+ }
165
+ }
166
+ async function runCachePurge(args) {
167
+ const json = flagEnabled(args, "json");
168
+ const quiet = flagEnabled(args, "quiet");
169
+ const dryRun = flagEnabled(args, "dry-run", "dryRun");
170
+ const request = resolveCachePurgeRequest(args);
171
+ const container = await createRequestContainer();
172
+ try {
173
+ const em = container.resolve("em");
174
+ const cache = container.resolve("cache");
175
+ const scopes = await resolveCacheScopes(em, args);
176
+ const results = [];
177
+ for (const scope of scopes) {
178
+ const result = await runWithCacheTenant(
179
+ scope.tenantId,
180
+ async () => dryRun ? previewCachePurge(cache, request) : executeCachePurge(cache, request)
181
+ );
182
+ results.push({
183
+ scope: scope.label,
184
+ dryRun,
185
+ request,
186
+ deleted: result.deleted,
187
+ keyCount: result.keys.length,
188
+ keys: result.keys,
189
+ note: result.note
190
+ });
191
+ }
192
+ if (json) {
193
+ console.log(JSON.stringify(results, null, 2));
194
+ return;
195
+ }
196
+ if (quiet) {
197
+ return;
198
+ }
199
+ for (const result of results) {
200
+ console.log(`${result.dryRun ? "\u{1F9EA}" : "\u{1F9F9}"} [cache] scope=${result.scope} deleted=${result.deleted}${result.dryRun ? " (dry-run)" : ""}`);
201
+ if (result.note) console.log(` \u2139\uFE0F note: ${result.note}`);
202
+ if (result.keys.length > 0) {
203
+ for (const key of result.keys) {
204
+ console.log(` \u2022 ${key}`);
205
+ }
206
+ }
207
+ }
208
+ } finally {
209
+ await disposeContainer(container);
210
+ }
211
+ }
212
+ async function runStructuralCachePurge(args) {
213
+ const nextArgs = {
214
+ ...args,
215
+ pattern: "nav:*"
216
+ };
217
+ await runCachePurge(nextArgs);
218
+ }
4
219
  function envDisablesAutoIndexing() {
5
220
  const raw = process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING;
6
221
  if (!raw) return false;
@@ -49,11 +264,39 @@ const restoreDefaults = {
49
264
  const help = {
50
265
  command: "help",
51
266
  async run() {
52
- console.log("Usage: yarn mercato configs restore-defaults");
267
+ console.log("\u2699\uFE0F Configs CLI");
268
+ console.log("");
269
+ console.log("\u{1F680} Usage: yarn mercato configs restore-defaults");
53
270
  console.log(" Ensures global module configuration defaults exist.");
271
+ console.log("");
272
+ printCacheHelp();
273
+ }
274
+ };
275
+ const cacheCommand = {
276
+ command: "cache",
277
+ async run(rest) {
278
+ const [subcommand, ...subRest] = rest;
279
+ const args = parseArgs(subRest);
280
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
281
+ printCacheHelp();
282
+ return;
283
+ }
284
+ if (subcommand === "stats") {
285
+ await runCacheStats(args);
286
+ return;
287
+ }
288
+ if (subcommand === "purge") {
289
+ await runCachePurge(args);
290
+ return;
291
+ }
292
+ if (subcommand === "structural") {
293
+ await runStructuralCachePurge(args);
294
+ return;
295
+ }
296
+ throw new Error(`Unknown cache subcommand "${subcommand}".`);
54
297
  }
55
298
  };
56
- var cli_default = [restoreDefaults, help];
299
+ var cli_default = [restoreDefaults, cacheCommand, help];
57
300
  export {
58
301
  cli_default as default
59
302
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/configs/cli.ts"],
4
- "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from './lib/module-config-service'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'\n\nfunction envDisablesAutoIndexing(): boolean {\n const raw = process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING\n if (!raw) return false\n return parseBooleanToken(raw) === true\n}\n\nconst restoreDefaults: ModuleCli = {\n command: 'restore-defaults',\n async run() {\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n console.error('[configs] moduleConfigService is not registered in the container.')\n return\n }\n\n const disabledByEnv = envDisablesAutoIndexing()\n const defaultEnabled = !disabledByEnv\n await service.restoreDefaults(\n [\n {\n moduleId: 'vector',\n name: 'auto_index_enabled',\n value: defaultEnabled,\n },\n {\n moduleId: 'notifications',\n name: NOTIFICATIONS_DELIVERY_CONFIG_KEY,\n value: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n },\n ],\n { force: true },\n )\n console.log(\n `[configs] Vector auto-indexing default set to ${defaultEnabled ? 'enabled' : 'disabled'}${\n disabledByEnv ? ' (forced by DISABLE_VECTOR_SEARCH_AUTOINDEXING)' : ''\n }.`,\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst help: ModuleCli = {\n command: 'help',\n async run() {\n console.log('Usage: yarn mercato configs restore-defaults')\n console.log(' Ensures global module configuration defaults exist.')\n },\n}\n\nexport default [restoreDefaults, help]\n"],
5
- "mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,yBAAyB;AAClC,SAAS,sCAAsC,yCAAyC;AAExF,SAAS,0BAAmC;AAC1C,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,kBAAkB,GAAG,MAAM;AACpC;AAEA,MAAM,kBAA6B;AAAA,EACjC,SAAS;AAAA,EACT,MAAM,MAAM;AACV,UAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAI;AACF,UAAI;AACJ,UAAI;AACF,kBAAW,UAAU,QAAQ,qBAAqB;AAAA,MACpD,QAAQ;AACN,gBAAQ,MAAM,mEAAmE;AACjF;AAAA,MACF;AAEA,YAAM,gBAAgB,wBAAwB;AAC9C,YAAM,iBAAiB,CAAC;AACxB,YAAM,QAAQ;AAAA,QACZ;AAAA,UACE;AAAA,YACE,UAAU;AAAA,YACV,MAAM;AAAA,YACN,OAAO;AAAA,UACT;AAAA,UACA;AAAA,YACE,UAAU;AAAA,YACV,MAAM;AAAA,YACN,OAAO;AAAA,UACT;AAAA,QACF;AAAA,QACA,EAAE,OAAO,KAAK;AAAA,MAChB;AACA,cAAQ;AAAA,QACN,iDAAiD,iBAAiB,YAAY,UAAU,GACtF,gBAAgB,oDAAoD,EACtE;AAAA,MACF;AAAA,IACF,UAAE;AACA,YAAM,aAAa;AACnB,UAAI,OAAO,WAAW,YAAY,YAAY;AAC5C,cAAM,WAAW,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,OAAkB;AAAA,EACtB,SAAS;AAAA,EACT,MAAM,MAAM;AACV,YAAQ,IAAI,8CAA8C;AAC1D,YAAQ,IAAI,uDAAuD;AAAA,EACrE;AACF;AAEA,IAAO,cAAQ,CAAC,iBAAiB,IAAI;",
4
+ "sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from './lib/module-config-service'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'\nimport { Tenant } from '../directory/data/entities'\nimport {\n collectCacheStats,\n executeCachePurge,\n previewCachePurge,\n type CachePurgeRequest,\n} from './lib/cache-cli'\n\ntype ParsedArgs = Record<string, string | boolean>\n\ntype CacheScope = {\n label: string\n tenantId: string | null\n}\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {\n args[rawKey] = rest[i + 1]!\n i += 1\n } else {\n args[rawKey] = true\n }\n }\n return args\n}\n\nfunction stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction flagEnabled(args: ParsedArgs, ...keys: string[]): boolean {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (typeof raw === 'string') {\n const parsed = parseBooleanToken(raw)\n return parsed === null ? true : parsed\n }\n }\n return false\n}\n\nfunction splitListOption(raw: string | undefined): string[] {\n if (!raw) return []\n const seen = new Set<string>()\n const values: string[] = []\n for (const item of raw.split(',')) {\n const trimmed = item.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n values.push(trimmed)\n }\n return values\n}\n\nasync function resolveCacheScopes(\n em: EntityManager,\n args: ParsedArgs,\n): Promise<CacheScope[]> {\n const explicitTenantId = stringOption(args, 'tenant', 'tenantId')\n const globalOnly = flagEnabled(args, 'global')\n const allTenants = flagEnabled(args, 'all-tenants', 'allTenants')\n\n if (explicitTenantId && globalOnly) {\n throw new Error('Cannot combine `--tenant` with `--global`.')\n }\n if (explicitTenantId && allTenants) {\n throw new Error('Cannot combine `--tenant` with `--all-tenants`.')\n }\n if (globalOnly && allTenants) {\n throw new Error('Cannot combine `--global` with `--all-tenants`.')\n }\n\n if (explicitTenantId) {\n return [{ label: `tenant:${explicitTenantId}`, tenantId: explicitTenantId }]\n }\n\n if (globalOnly) {\n return [{ label: 'global', tenantId: null }]\n }\n\n if (!allTenants) {\n return [{ label: 'global', tenantId: null }]\n }\n\n const tenants = await em.find(Tenant, { deletedAt: null }, { orderBy: { name: 'asc' } })\n const scopes: CacheScope[] = [{ label: 'global', tenantId: null }]\n const seen = new Set<string>()\n for (const tenant of tenants) {\n const tenantId = typeof tenant.id === 'string' ? tenant.id : ''\n if (!tenantId || seen.has(tenantId)) continue\n seen.add(tenantId)\n scopes.push({ label: `tenant:${tenantId}`, tenantId })\n }\n return scopes\n}\n\nfunction resolveCachePurgeRequest(args: ParsedArgs): CachePurgeRequest {\n if (flagEnabled(args, 'all')) return { kind: 'all' }\n\n const segment = stringOption(args, 'segment')\n if (segment) return { kind: 'segment', segment }\n\n const tags = splitListOption(stringOption(args, 'tag', 'tags'))\n if (tags.length > 0) return { kind: 'tags', tags }\n\n const keys = splitListOption(stringOption(args, 'key', 'keys'))\n if (keys.length > 0) return { kind: 'keys', keys }\n\n const ids = splitListOption(stringOption(args, 'id', 'ids'))\n if (ids.length > 0) return { kind: 'ids', ids }\n\n const pattern = stringOption(args, 'pattern')\n if (pattern) return { kind: 'pattern', pattern }\n\n throw new Error(\n 'Choose a purge target: `--all`, `--segment <id>`, `--tag <tag1,tag2>`, `--key <key1,key2>`, `--id <token1,token2>`, or `--pattern <glob>`.',\n )\n}\n\nfunction printCacheHelp() {\n console.log('\uD83E\uDDF9 Cache CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage:')\n console.log(' yarn mercato configs cache stats [--tenant <id> | --global | --all-tenants] [--json]')\n console.log(' yarn mercato configs cache purge --all [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --segment <segment> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --tag <tag1,tag2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log('')\n console.log('\u2139\uFE0F Notes:')\n console.log(' `stats` mirrors the cache admin page segment overview for CRUD/widget caches.')\n console.log(' `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).')\n console.log(' `structural` targets navigation caches (`nav:*`) and is the recommended post-step after module/sidebar structure changes.')\n console.log(' When no scope flag is supplied, this command uses the global cache scope only.')\n}\n\nasync function disposeContainer(container: unknown) {\n const disposable = container as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n}\n\nasync function runCacheStats(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n for (const scope of scopes) {\n const stats = await runWithCacheTenant(scope.tenantId, async () => collectCacheStats(cache))\n results.push({ scope: scope.label, ...stats })\n }\n\n if (json) {\n console.log(JSON.stringify(results, null, 2))\n return\n }\n\n for (const result of results) {\n console.log(`\uD83D\uDD0E [cache] scope=${result.scope} totalKeys=${result.totalKeys} generatedAt=${result.generatedAt}`)\n if (result.segments.length === 0) {\n console.log(' \u2205 segments: none')\n continue\n }\n for (const segment of result.segments) {\n console.log(` \u2022 ${segment.segment} (${segment.keyCount})${segment.path ? ` ${segment.path}` : ''}`)\n }\n }\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runCachePurge(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const quiet = flagEnabled(args, 'quiet')\n const dryRun = flagEnabled(args, 'dry-run', 'dryRun')\n const request = resolveCachePurgeRequest(args)\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n\n for (const scope of scopes) {\n const result = await runWithCacheTenant(scope.tenantId, async () =>\n dryRun ? previewCachePurge(cache, request) : executeCachePurge(cache, request)\n )\n results.push({\n scope: scope.label,\n dryRun,\n request,\n deleted: result.deleted,\n keyCount: result.keys.length,\n keys: result.keys,\n note: result.note,\n })\n }\n\n if (json) {\n console.log(JSON.stringify(results, null, 2))\n return\n }\n\n if (quiet) {\n return\n }\n\n for (const result of results) {\n console.log(`${result.dryRun ? '\uD83E\uDDEA' : '\uD83E\uDDF9'} [cache] scope=${result.scope} deleted=${result.deleted}${result.dryRun ? ' (dry-run)' : ''}`)\n if (result.note) console.log(` \u2139\uFE0F note: ${result.note}`)\n if (result.keys.length > 0) {\n for (const key of result.keys) {\n console.log(` \u2022 ${key}`)\n }\n }\n }\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runStructuralCachePurge(args: ParsedArgs) {\n const nextArgs: ParsedArgs = {\n ...args,\n pattern: 'nav:*',\n }\n await runCachePurge(nextArgs)\n}\n\nfunction envDisablesAutoIndexing(): boolean {\n const raw = process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING\n if (!raw) return false\n return parseBooleanToken(raw) === true\n}\n\nconst restoreDefaults: ModuleCli = {\n command: 'restore-defaults',\n async run() {\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n console.error('[configs] moduleConfigService is not registered in the container.')\n return\n }\n\n const disabledByEnv = envDisablesAutoIndexing()\n const defaultEnabled = !disabledByEnv\n await service.restoreDefaults(\n [\n {\n moduleId: 'vector',\n name: 'auto_index_enabled',\n value: defaultEnabled,\n },\n {\n moduleId: 'notifications',\n name: NOTIFICATIONS_DELIVERY_CONFIG_KEY,\n value: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n },\n ],\n { force: true },\n )\n console.log(\n `[configs] Vector auto-indexing default set to ${defaultEnabled ? 'enabled' : 'disabled'}${\n disabledByEnv ? ' (forced by DISABLE_VECTOR_SEARCH_AUTOINDEXING)' : ''\n }.`,\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst help: ModuleCli = {\n command: 'help',\n async run() {\n console.log('\u2699\uFE0F Configs CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage: yarn mercato configs restore-defaults')\n console.log(' Ensures global module configuration defaults exist.')\n console.log('')\n printCacheHelp()\n },\n}\n\nconst cacheCommand: ModuleCli = {\n command: 'cache',\n async run(rest) {\n const [subcommand, ...subRest] = rest\n const args = parseArgs(subRest)\n\n if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {\n printCacheHelp()\n return\n }\n\n if (subcommand === 'stats') {\n await runCacheStats(args)\n return\n }\n\n if (subcommand === 'purge') {\n await runCachePurge(args)\n return\n }\n\n if (subcommand === 'structural') {\n await runStructuralCachePurge(args)\n return\n }\n\n throw new Error(`Unknown cache subcommand \"${subcommand}\".`)\n },\n}\n\nexport default [restoreDefaults, cacheCommand, help]\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,143 @@
1
+ import { collectCrudCacheStats, purgeCrudCacheSegment } from "@open-mercato/shared/lib/crud/cache-stats";
2
+ function normalizeUnique(values) {
3
+ const seen = /* @__PURE__ */ new Set();
4
+ const normalized = [];
5
+ for (const value of values) {
6
+ const trimmed = value.trim();
7
+ if (!trimmed || seen.has(trimmed)) continue;
8
+ seen.add(trimmed);
9
+ normalized.push(trimmed);
10
+ }
11
+ return normalized;
12
+ }
13
+ async function deleteKeys(cache, keys) {
14
+ let deleted = 0;
15
+ for (const key of keys) {
16
+ const removed = await cache.delete(key);
17
+ if (removed) deleted += 1;
18
+ }
19
+ return deleted;
20
+ }
21
+ async function resolveIdentifierKeys(cache, ids) {
22
+ const matches = /* @__PURE__ */ new Set();
23
+ for (const id of normalizeUnique(ids)) {
24
+ const keys = await cache.keys(`*${id}*`);
25
+ for (const key of keys) matches.add(key);
26
+ }
27
+ return Array.from(matches).sort((a, b) => a.localeCompare(b));
28
+ }
29
+ async function collectCacheStats(cache) {
30
+ const stats = await collectCrudCacheStats(cache);
31
+ return {
32
+ generatedAt: stats.generatedAt,
33
+ totalKeys: stats.totalKeys,
34
+ segments: stats.segments.map((segment) => ({
35
+ segment: segment.segment,
36
+ keyCount: segment.keyCount,
37
+ method: segment.method,
38
+ path: segment.path,
39
+ resource: segment.resource
40
+ }))
41
+ };
42
+ }
43
+ async function previewCachePurge(cache, request) {
44
+ if (request.kind === "all") {
45
+ const keys2 = (await cache.keys()).sort((a, b) => a.localeCompare(b));
46
+ return { deleted: keys2.length, keys: keys2, note: null };
47
+ }
48
+ if (request.kind === "segment") {
49
+ const stats = await collectCrudCacheStats(cache);
50
+ const target = stats.segments.find((segment) => segment.segment === request.segment);
51
+ return {
52
+ deleted: target?.keys.length ?? 0,
53
+ keys: (target?.keys ?? []).slice().sort((a, b) => a.localeCompare(b)),
54
+ note: target ? null : `Cache segment "${request.segment}" was not found in this scope.`
55
+ };
56
+ }
57
+ if (request.kind === "tags") {
58
+ return {
59
+ deleted: 0,
60
+ keys: [],
61
+ note: "Tag purges do not expose matching keys through the cache interface. Run without `--dry-run` to execute."
62
+ };
63
+ }
64
+ if (request.kind === "keys") {
65
+ const requested = normalizeUnique(request.keys);
66
+ const existingKeys = await cache.keys();
67
+ const existingSet = new Set(existingKeys);
68
+ const keys2 = requested.filter((key) => existingSet.has(key));
69
+ return {
70
+ deleted: keys2.length,
71
+ keys: keys2,
72
+ note: keys2.length === requested.length ? null : "Some requested keys were not present in this scope."
73
+ };
74
+ }
75
+ if (request.kind === "ids") {
76
+ const keys2 = await resolveIdentifierKeys(cache, request.ids);
77
+ return {
78
+ deleted: keys2.length,
79
+ keys: keys2,
80
+ note: keys2.length > 0 ? null : "No cache keys matched the requested identifier tokens."
81
+ };
82
+ }
83
+ const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b));
84
+ return {
85
+ deleted: keys.length,
86
+ keys,
87
+ note: keys.length > 0 ? null : `No cache keys matched pattern "${request.pattern}".`
88
+ };
89
+ }
90
+ async function executeCachePurge(cache, request) {
91
+ if (request.kind === "all") {
92
+ const keys2 = (await cache.keys()).sort((a, b) => a.localeCompare(b));
93
+ const deleted2 = await cache.clear();
94
+ return { deleted: deleted2, keys: keys2, note: null };
95
+ }
96
+ if (request.kind === "segment") {
97
+ const result = await purgeCrudCacheSegment(cache, request.segment);
98
+ return {
99
+ deleted: result.deleted,
100
+ keys: result.keys.slice().sort((a, b) => a.localeCompare(b)),
101
+ note: result.keys.length > 0 ? null : `Cache segment "${request.segment}" was not found in this scope.`
102
+ };
103
+ }
104
+ if (request.kind === "tags") {
105
+ const deleted2 = await cache.deleteByTags(normalizeUnique(request.tags));
106
+ return {
107
+ deleted: deleted2,
108
+ keys: [],
109
+ note: "Tag purges report counts only because the cache interface does not expose tag-to-key listings."
110
+ };
111
+ }
112
+ if (request.kind === "keys") {
113
+ const preview = await previewCachePurge(cache, request);
114
+ const deleted2 = await deleteKeys(cache, preview.keys);
115
+ return {
116
+ deleted: deleted2,
117
+ keys: preview.keys,
118
+ note: preview.note
119
+ };
120
+ }
121
+ if (request.kind === "ids") {
122
+ const keys2 = await resolveIdentifierKeys(cache, request.ids);
123
+ const deleted2 = await deleteKeys(cache, keys2);
124
+ return {
125
+ deleted: deleted2,
126
+ keys: keys2,
127
+ note: keys2.length > 0 ? null : "No cache keys matched the requested identifier tokens."
128
+ };
129
+ }
130
+ const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b));
131
+ const deleted = await deleteKeys(cache, keys);
132
+ return {
133
+ deleted,
134
+ keys,
135
+ note: keys.length > 0 ? null : `No cache keys matched pattern "${request.pattern}".`
136
+ };
137
+ }
138
+ export {
139
+ collectCacheStats,
140
+ executeCachePurge,
141
+ previewCachePurge
142
+ };
143
+ //# sourceMappingURL=cache-cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/configs/lib/cache-cli.ts"],
4
+ "sourcesContent": ["import type { CacheStrategy } from '@open-mercato/cache'\nimport { collectCrudCacheStats, purgeCrudCacheSegment } from '@open-mercato/shared/lib/crud/cache-stats'\n\nexport type CachePurgeRequest =\n | { kind: 'all' }\n | { kind: 'segment'; segment: string }\n | { kind: 'tags'; tags: string[] }\n | { kind: 'keys'; keys: string[] }\n | { kind: 'ids'; ids: string[] }\n | { kind: 'pattern'; pattern: string }\n\nexport type CachePurgeResult = {\n deleted: number\n keys: string[]\n note: string | null\n}\n\nfunction normalizeUnique(values: string[]): string[] {\n const seen = new Set<string>()\n const normalized: string[] = []\n for (const value of values) {\n const trimmed = value.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n normalized.push(trimmed)\n }\n return normalized\n}\n\nasync function deleteKeys(cache: CacheStrategy, keys: string[]): Promise<number> {\n let deleted = 0\n for (const key of keys) {\n const removed = await cache.delete(key)\n if (removed) deleted += 1\n }\n return deleted\n}\n\nasync function resolveIdentifierKeys(cache: CacheStrategy, ids: string[]): Promise<string[]> {\n const matches = new Set<string>()\n for (const id of normalizeUnique(ids)) {\n const keys = await cache.keys(`*${id}*`)\n for (const key of keys) matches.add(key)\n }\n return Array.from(matches).sort((a, b) => a.localeCompare(b))\n}\n\nexport async function collectCacheStats(cache: CacheStrategy) {\n const stats = await collectCrudCacheStats(cache)\n return {\n generatedAt: stats.generatedAt,\n totalKeys: stats.totalKeys,\n segments: stats.segments.map((segment) => ({\n segment: segment.segment,\n keyCount: segment.keyCount,\n method: segment.method,\n path: segment.path,\n resource: segment.resource,\n })),\n }\n}\n\nexport async function previewCachePurge(\n cache: CacheStrategy,\n request: CachePurgeRequest,\n): Promise<CachePurgeResult> {\n if (request.kind === 'all') {\n const keys = (await cache.keys()).sort((a, b) => a.localeCompare(b))\n return { deleted: keys.length, keys, note: null }\n }\n\n if (request.kind === 'segment') {\n const stats = await collectCrudCacheStats(cache)\n const target = stats.segments.find((segment) => segment.segment === request.segment)\n return {\n deleted: target?.keys.length ?? 0,\n keys: (target?.keys ?? []).slice().sort((a, b) => a.localeCompare(b)),\n note: target ? null : `Cache segment \"${request.segment}\" was not found in this scope.`,\n }\n }\n\n if (request.kind === 'tags') {\n return {\n deleted: 0,\n keys: [],\n note: 'Tag purges do not expose matching keys through the cache interface. Run without `--dry-run` to execute.',\n }\n }\n\n if (request.kind === 'keys') {\n const requested = normalizeUnique(request.keys)\n const existingKeys = await cache.keys()\n const existingSet = new Set(existingKeys)\n const keys = requested.filter((key) => existingSet.has(key))\n return {\n deleted: keys.length,\n keys,\n note: keys.length === requested.length ? null : 'Some requested keys were not present in this scope.',\n }\n }\n\n if (request.kind === 'ids') {\n const keys = await resolveIdentifierKeys(cache, request.ids)\n return {\n deleted: keys.length,\n keys,\n note: keys.length > 0 ? null : 'No cache keys matched the requested identifier tokens.',\n }\n }\n\n const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b))\n return {\n deleted: keys.length,\n keys,\n note: keys.length > 0 ? null : `No cache keys matched pattern \"${request.pattern}\".`,\n }\n}\n\nexport async function executeCachePurge(\n cache: CacheStrategy,\n request: CachePurgeRequest,\n): Promise<CachePurgeResult> {\n if (request.kind === 'all') {\n const keys = (await cache.keys()).sort((a, b) => a.localeCompare(b))\n const deleted = await cache.clear()\n return { deleted, keys, note: null }\n }\n\n if (request.kind === 'segment') {\n const result = await purgeCrudCacheSegment(cache, request.segment)\n return {\n deleted: result.deleted,\n keys: result.keys.slice().sort((a, b) => a.localeCompare(b)),\n note: result.keys.length > 0 ? null : `Cache segment \"${request.segment}\" was not found in this scope.`,\n }\n }\n\n if (request.kind === 'tags') {\n const deleted = await cache.deleteByTags(normalizeUnique(request.tags))\n return {\n deleted,\n keys: [],\n note: 'Tag purges report counts only because the cache interface does not expose tag-to-key listings.',\n }\n }\n\n if (request.kind === 'keys') {\n const preview = await previewCachePurge(cache, request)\n const deleted = await deleteKeys(cache, preview.keys)\n return {\n deleted,\n keys: preview.keys,\n note: preview.note,\n }\n }\n\n if (request.kind === 'ids') {\n const keys = await resolveIdentifierKeys(cache, request.ids)\n const deleted = await deleteKeys(cache, keys)\n return {\n deleted,\n keys,\n note: keys.length > 0 ? null : 'No cache keys matched the requested identifier tokens.',\n }\n }\n\n const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b))\n const deleted = await deleteKeys(cache, keys)\n return {\n deleted,\n keys,\n note: keys.length > 0 ? null : `No cache keys matched pattern \"${request.pattern}\".`,\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,uBAAuB,6BAA6B;AAgB7D,SAAS,gBAAgB,QAA4B;AACnD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,aAAuB,CAAC;AAC9B,aAAW,SAAS,QAAQ;AAC1B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,KAAK,IAAI,OAAO,EAAG;AACnC,SAAK,IAAI,OAAO;AAChB,eAAW,KAAK,OAAO;AAAA,EACzB;AACA,SAAO;AACT;AAEA,eAAe,WAAW,OAAsB,MAAiC;AAC/E,MAAI,UAAU;AACd,aAAW,OAAO,MAAM;AACtB,UAAM,UAAU,MAAM,MAAM,OAAO,GAAG;AACtC,QAAI,QAAS,YAAW;AAAA,EAC1B;AACA,SAAO;AACT;AAEA,eAAe,sBAAsB,OAAsB,KAAkC;AAC3F,QAAM,UAAU,oBAAI,IAAY;AAChC,aAAW,MAAM,gBAAgB,GAAG,GAAG;AACrC,UAAM,OAAO,MAAM,MAAM,KAAK,IAAI,EAAE,GAAG;AACvC,eAAW,OAAO,KAAM,SAAQ,IAAI,GAAG;AAAA,EACzC;AACA,SAAO,MAAM,KAAK,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAC9D;AAEA,eAAsB,kBAAkB,OAAsB;AAC5D,QAAM,QAAQ,MAAM,sBAAsB,KAAK;AAC/C,SAAO;AAAA,IACL,aAAa,MAAM;AAAA,IACnB,WAAW,MAAM;AAAA,IACjB,UAAU,MAAM,SAAS,IAAI,CAAC,aAAa;AAAA,MACzC,SAAS,QAAQ;AAAA,MACjB,UAAU,QAAQ;AAAA,MAClB,QAAQ,QAAQ;AAAA,MAChB,MAAM,QAAQ;AAAA,MACd,UAAU,QAAQ;AAAA,IACpB,EAAE;AAAA,EACJ;AACF;AAEA,eAAsB,kBACpB,OACA,SAC2B;AAC3B,MAAI,QAAQ,SAAS,OAAO;AAC1B,UAAMA,SAAQ,MAAM,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACnE,WAAO,EAAE,SAASA,MAAK,QAAQ,MAAAA,OAAM,MAAM,KAAK;AAAA,EAClD;AAEA,MAAI,QAAQ,SAAS,WAAW;AAC9B,UAAM,QAAQ,MAAM,sBAAsB,KAAK;AAC/C,UAAM,SAAS,MAAM,SAAS,KAAK,CAAC,YAAY,QAAQ,YAAY,QAAQ,OAAO;AACnF,WAAO;AAAA,MACL,SAAS,QAAQ,KAAK,UAAU;AAAA,MAChC,OAAO,QAAQ,QAAQ,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,MACpE,MAAM,SAAS,OAAO,kBAAkB,QAAQ,OAAO;AAAA,IACzD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,CAAC;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,YAAY,gBAAgB,QAAQ,IAAI;AAC9C,UAAM,eAAe,MAAM,MAAM,KAAK;AACtC,UAAM,cAAc,IAAI,IAAI,YAAY;AACxC,UAAMA,QAAO,UAAU,OAAO,CAAC,QAAQ,YAAY,IAAI,GAAG,CAAC;AAC3D,WAAO;AAAA,MACL,SAASA,MAAK;AAAA,MACd,MAAAA;AAAA,MACA,MAAMA,MAAK,WAAW,UAAU,SAAS,OAAO;AAAA,IAClD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,OAAO;AAC1B,UAAMA,QAAO,MAAM,sBAAsB,OAAO,QAAQ,GAAG;AAC3D,WAAO;AAAA,MACL,SAASA,MAAK;AAAA,MACd,MAAAA;AAAA,MACA,MAAMA,MAAK,SAAS,IAAI,OAAO;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,MAAM,KAAK,QAAQ,OAAO,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAClF,SAAO;AAAA,IACL,SAAS,KAAK;AAAA,IACd;AAAA,IACA,MAAM,KAAK,SAAS,IAAI,OAAO,kCAAkC,QAAQ,OAAO;AAAA,EAClF;AACF;AAEA,eAAsB,kBACpB,OACA,SAC2B;AAC3B,MAAI,QAAQ,SAAS,OAAO;AAC1B,UAAMA,SAAQ,MAAM,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACnE,UAAMC,WAAU,MAAM,MAAM,MAAM;AAClC,WAAO,EAAE,SAAAA,UAAS,MAAAD,OAAM,MAAM,KAAK;AAAA,EACrC;AAEA,MAAI,QAAQ,SAAS,WAAW;AAC9B,UAAM,SAAS,MAAM,sBAAsB,OAAO,QAAQ,OAAO;AACjE,WAAO;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,MAC3D,MAAM,OAAO,KAAK,SAAS,IAAI,OAAO,kBAAkB,QAAQ,OAAO;AAAA,IACzE;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAMC,WAAU,MAAM,MAAM,aAAa,gBAAgB,QAAQ,IAAI,CAAC;AACtE,WAAO;AAAA,MACL,SAAAA;AAAA,MACA,MAAM,CAAC;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,UAAU,MAAM,kBAAkB,OAAO,OAAO;AACtD,UAAMA,WAAU,MAAM,WAAW,OAAO,QAAQ,IAAI;AACpD,WAAO;AAAA,MACL,SAAAA;AAAA,MACA,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,OAAO;AAC1B,UAAMD,QAAO,MAAM,sBAAsB,OAAO,QAAQ,GAAG;AAC3D,UAAMC,WAAU,MAAM,WAAW,OAAOD,KAAI;AAC5C,WAAO;AAAA,MACL,SAAAC;AAAA,MACA,MAAAD;AAAA,MACA,MAAMA,MAAK,SAAS,IAAI,OAAO;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,MAAM,KAAK,QAAQ,OAAO,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAClF,QAAM,UAAU,MAAM,WAAW,OAAO,IAAI;AAC5C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM,KAAK,SAAS,IAAI,OAAO,kCAAkC,QAAQ,OAAO;AAAA,EAClF;AACF;",
6
+ "names": ["keys", "deleted"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.11-develop.1416.adda6008da",
3
+ "version": "0.4.11-develop.1418.27a299bdaf",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -230,10 +230,10 @@
230
230
  "ts-pattern": "^5.0.0"
231
231
  },
232
232
  "peerDependencies": {
233
- "@open-mercato/shared": "0.4.11-develop.1416.adda6008da"
233
+ "@open-mercato/shared": "0.4.11-develop.1418.27a299bdaf"
234
234
  },
235
235
  "devDependencies": {
236
- "@open-mercato/shared": "0.4.11-develop.1416.adda6008da",
236
+ "@open-mercato/shared": "0.4.11-develop.1418.27a299bdaf",
237
237
  "@testing-library/dom": "^10.4.1",
238
238
  "@testing-library/jest-dom": "^6.9.1",
239
239
  "@testing-library/react": "^16.3.1",
@@ -1,8 +1,262 @@
1
1
  import type { ModuleCli } from '@open-mercato/shared/modules/registry'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
2
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
5
  import type { ModuleConfigService } from './lib/module-config-service'
4
6
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
5
7
  import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'
8
+ import { Tenant } from '../directory/data/entities'
9
+ import {
10
+ collectCacheStats,
11
+ executeCachePurge,
12
+ previewCachePurge,
13
+ type CachePurgeRequest,
14
+ } from './lib/cache-cli'
15
+
16
+ type ParsedArgs = Record<string, string | boolean>
17
+
18
+ type CacheScope = {
19
+ label: string
20
+ tenantId: string | null
21
+ }
22
+
23
+ function parseArgs(rest: string[]): ParsedArgs {
24
+ const args: ParsedArgs = {}
25
+ for (let i = 0; i < rest.length; i += 1) {
26
+ const part = rest[i]
27
+ if (!part?.startsWith('--')) continue
28
+ const [rawKey, rawValue] = part.slice(2).split('=')
29
+ if (!rawKey) continue
30
+ if (rawValue !== undefined) {
31
+ args[rawKey] = rawValue
32
+ } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {
33
+ args[rawKey] = rest[i + 1]!
34
+ i += 1
35
+ } else {
36
+ args[rawKey] = true
37
+ }
38
+ }
39
+ return args
40
+ }
41
+
42
+ function stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {
43
+ for (const key of keys) {
44
+ const raw = args[key]
45
+ if (typeof raw !== 'string') continue
46
+ const trimmed = raw.trim()
47
+ if (trimmed.length > 0) return trimmed
48
+ }
49
+ return undefined
50
+ }
51
+
52
+ function flagEnabled(args: ParsedArgs, ...keys: string[]): boolean {
53
+ for (const key of keys) {
54
+ const raw = args[key]
55
+ if (raw === undefined) continue
56
+ if (raw === true) return true
57
+ if (typeof raw === 'string') {
58
+ const parsed = parseBooleanToken(raw)
59
+ return parsed === null ? true : parsed
60
+ }
61
+ }
62
+ return false
63
+ }
64
+
65
+ function splitListOption(raw: string | undefined): string[] {
66
+ if (!raw) return []
67
+ const seen = new Set<string>()
68
+ const values: string[] = []
69
+ for (const item of raw.split(',')) {
70
+ const trimmed = item.trim()
71
+ if (!trimmed || seen.has(trimmed)) continue
72
+ seen.add(trimmed)
73
+ values.push(trimmed)
74
+ }
75
+ return values
76
+ }
77
+
78
+ async function resolveCacheScopes(
79
+ em: EntityManager,
80
+ args: ParsedArgs,
81
+ ): Promise<CacheScope[]> {
82
+ const explicitTenantId = stringOption(args, 'tenant', 'tenantId')
83
+ const globalOnly = flagEnabled(args, 'global')
84
+ const allTenants = flagEnabled(args, 'all-tenants', 'allTenants')
85
+
86
+ if (explicitTenantId && globalOnly) {
87
+ throw new Error('Cannot combine `--tenant` with `--global`.')
88
+ }
89
+ if (explicitTenantId && allTenants) {
90
+ throw new Error('Cannot combine `--tenant` with `--all-tenants`.')
91
+ }
92
+ if (globalOnly && allTenants) {
93
+ throw new Error('Cannot combine `--global` with `--all-tenants`.')
94
+ }
95
+
96
+ if (explicitTenantId) {
97
+ return [{ label: `tenant:${explicitTenantId}`, tenantId: explicitTenantId }]
98
+ }
99
+
100
+ if (globalOnly) {
101
+ return [{ label: 'global', tenantId: null }]
102
+ }
103
+
104
+ if (!allTenants) {
105
+ return [{ label: 'global', tenantId: null }]
106
+ }
107
+
108
+ const tenants = await em.find(Tenant, { deletedAt: null }, { orderBy: { name: 'asc' } })
109
+ const scopes: CacheScope[] = [{ label: 'global', tenantId: null }]
110
+ const seen = new Set<string>()
111
+ for (const tenant of tenants) {
112
+ const tenantId = typeof tenant.id === 'string' ? tenant.id : ''
113
+ if (!tenantId || seen.has(tenantId)) continue
114
+ seen.add(tenantId)
115
+ scopes.push({ label: `tenant:${tenantId}`, tenantId })
116
+ }
117
+ return scopes
118
+ }
119
+
120
+ function resolveCachePurgeRequest(args: ParsedArgs): CachePurgeRequest {
121
+ if (flagEnabled(args, 'all')) return { kind: 'all' }
122
+
123
+ const segment = stringOption(args, 'segment')
124
+ if (segment) return { kind: 'segment', segment }
125
+
126
+ const tags = splitListOption(stringOption(args, 'tag', 'tags'))
127
+ if (tags.length > 0) return { kind: 'tags', tags }
128
+
129
+ const keys = splitListOption(stringOption(args, 'key', 'keys'))
130
+ if (keys.length > 0) return { kind: 'keys', keys }
131
+
132
+ const ids = splitListOption(stringOption(args, 'id', 'ids'))
133
+ if (ids.length > 0) return { kind: 'ids', ids }
134
+
135
+ const pattern = stringOption(args, 'pattern')
136
+ if (pattern) return { kind: 'pattern', pattern }
137
+
138
+ throw new Error(
139
+ 'Choose a purge target: `--all`, `--segment <id>`, `--tag <tag1,tag2>`, `--key <key1,key2>`, `--id <token1,token2>`, or `--pattern <glob>`.',
140
+ )
141
+ }
142
+
143
+ function printCacheHelp() {
144
+ console.log('🧹 Cache CLI')
145
+ console.log('')
146
+ console.log('🚀 Usage:')
147
+ console.log(' yarn mercato configs cache stats [--tenant <id> | --global | --all-tenants] [--json]')
148
+ console.log(' yarn mercato configs cache purge --all [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
149
+ console.log(' yarn mercato configs cache purge --segment <segment> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
150
+ console.log(' yarn mercato configs cache purge --tag <tag1,tag2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
151
+ console.log(' yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
152
+ console.log(' yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
153
+ console.log(' yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
154
+ console.log(' yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')
155
+ console.log('')
156
+ console.log('ℹ️ Notes:')
157
+ console.log(' `stats` mirrors the cache admin page segment overview for CRUD/widget caches.')
158
+ console.log(' `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).')
159
+ console.log(' `structural` targets navigation caches (`nav:*`) and is the recommended post-step after module/sidebar structure changes.')
160
+ console.log(' When no scope flag is supplied, this command uses the global cache scope only.')
161
+ }
162
+
163
+ async function disposeContainer(container: unknown) {
164
+ const disposable = container as { dispose?: () => Promise<void> }
165
+ if (typeof disposable.dispose === 'function') {
166
+ await disposable.dispose()
167
+ }
168
+ }
169
+
170
+ async function runCacheStats(args: ParsedArgs) {
171
+ const json = flagEnabled(args, 'json')
172
+ const container = await createRequestContainer()
173
+ try {
174
+ const em = container.resolve('em') as EntityManager
175
+ const cache = container.resolve('cache') as CacheStrategy
176
+ const scopes = await resolveCacheScopes(em, args)
177
+ const results = []
178
+ for (const scope of scopes) {
179
+ const stats = await runWithCacheTenant(scope.tenantId, async () => collectCacheStats(cache))
180
+ results.push({ scope: scope.label, ...stats })
181
+ }
182
+
183
+ if (json) {
184
+ console.log(JSON.stringify(results, null, 2))
185
+ return
186
+ }
187
+
188
+ for (const result of results) {
189
+ console.log(`🔎 [cache] scope=${result.scope} totalKeys=${result.totalKeys} generatedAt=${result.generatedAt}`)
190
+ if (result.segments.length === 0) {
191
+ console.log(' ∅ segments: none')
192
+ continue
193
+ }
194
+ for (const segment of result.segments) {
195
+ console.log(` • ${segment.segment} (${segment.keyCount})${segment.path ? ` ${segment.path}` : ''}`)
196
+ }
197
+ }
198
+ } finally {
199
+ await disposeContainer(container)
200
+ }
201
+ }
202
+
203
+ async function runCachePurge(args: ParsedArgs) {
204
+ const json = flagEnabled(args, 'json')
205
+ const quiet = flagEnabled(args, 'quiet')
206
+ const dryRun = flagEnabled(args, 'dry-run', 'dryRun')
207
+ const request = resolveCachePurgeRequest(args)
208
+ const container = await createRequestContainer()
209
+ try {
210
+ const em = container.resolve('em') as EntityManager
211
+ const cache = container.resolve('cache') as CacheStrategy
212
+ const scopes = await resolveCacheScopes(em, args)
213
+ const results = []
214
+
215
+ for (const scope of scopes) {
216
+ const result = await runWithCacheTenant(scope.tenantId, async () =>
217
+ dryRun ? previewCachePurge(cache, request) : executeCachePurge(cache, request)
218
+ )
219
+ results.push({
220
+ scope: scope.label,
221
+ dryRun,
222
+ request,
223
+ deleted: result.deleted,
224
+ keyCount: result.keys.length,
225
+ keys: result.keys,
226
+ note: result.note,
227
+ })
228
+ }
229
+
230
+ if (json) {
231
+ console.log(JSON.stringify(results, null, 2))
232
+ return
233
+ }
234
+
235
+ if (quiet) {
236
+ return
237
+ }
238
+
239
+ for (const result of results) {
240
+ console.log(`${result.dryRun ? '🧪' : '🧹'} [cache] scope=${result.scope} deleted=${result.deleted}${result.dryRun ? ' (dry-run)' : ''}`)
241
+ if (result.note) console.log(` ℹ️ note: ${result.note}`)
242
+ if (result.keys.length > 0) {
243
+ for (const key of result.keys) {
244
+ console.log(` • ${key}`)
245
+ }
246
+ }
247
+ }
248
+ } finally {
249
+ await disposeContainer(container)
250
+ }
251
+ }
252
+
253
+ async function runStructuralCachePurge(args: ParsedArgs) {
254
+ const nextArgs: ParsedArgs = {
255
+ ...args,
256
+ pattern: 'nav:*',
257
+ }
258
+ await runCachePurge(nextArgs)
259
+ }
6
260
 
7
261
  function envDisablesAutoIndexing(): boolean {
8
262
  const raw = process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING
@@ -57,9 +311,43 @@ const restoreDefaults: ModuleCli = {
57
311
  const help: ModuleCli = {
58
312
  command: 'help',
59
313
  async run() {
60
- console.log('Usage: yarn mercato configs restore-defaults')
314
+ console.log('⚙️ Configs CLI')
315
+ console.log('')
316
+ console.log('🚀 Usage: yarn mercato configs restore-defaults')
61
317
  console.log(' Ensures global module configuration defaults exist.')
318
+ console.log('')
319
+ printCacheHelp()
320
+ },
321
+ }
322
+
323
+ const cacheCommand: ModuleCli = {
324
+ command: 'cache',
325
+ async run(rest) {
326
+ const [subcommand, ...subRest] = rest
327
+ const args = parseArgs(subRest)
328
+
329
+ if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
330
+ printCacheHelp()
331
+ return
332
+ }
333
+
334
+ if (subcommand === 'stats') {
335
+ await runCacheStats(args)
336
+ return
337
+ }
338
+
339
+ if (subcommand === 'purge') {
340
+ await runCachePurge(args)
341
+ return
342
+ }
343
+
344
+ if (subcommand === 'structural') {
345
+ await runStructuralCachePurge(args)
346
+ return
347
+ }
348
+
349
+ throw new Error(`Unknown cache subcommand "${subcommand}".`)
62
350
  },
63
351
  }
64
352
 
65
- export default [restoreDefaults, help]
353
+ export default [restoreDefaults, cacheCommand, help]
@@ -0,0 +1,174 @@
1
+ import type { CacheStrategy } from '@open-mercato/cache'
2
+ import { collectCrudCacheStats, purgeCrudCacheSegment } from '@open-mercato/shared/lib/crud/cache-stats'
3
+
4
+ export type CachePurgeRequest =
5
+ | { kind: 'all' }
6
+ | { kind: 'segment'; segment: string }
7
+ | { kind: 'tags'; tags: string[] }
8
+ | { kind: 'keys'; keys: string[] }
9
+ | { kind: 'ids'; ids: string[] }
10
+ | { kind: 'pattern'; pattern: string }
11
+
12
+ export type CachePurgeResult = {
13
+ deleted: number
14
+ keys: string[]
15
+ note: string | null
16
+ }
17
+
18
+ function normalizeUnique(values: string[]): string[] {
19
+ const seen = new Set<string>()
20
+ const normalized: string[] = []
21
+ for (const value of values) {
22
+ const trimmed = value.trim()
23
+ if (!trimmed || seen.has(trimmed)) continue
24
+ seen.add(trimmed)
25
+ normalized.push(trimmed)
26
+ }
27
+ return normalized
28
+ }
29
+
30
+ async function deleteKeys(cache: CacheStrategy, keys: string[]): Promise<number> {
31
+ let deleted = 0
32
+ for (const key of keys) {
33
+ const removed = await cache.delete(key)
34
+ if (removed) deleted += 1
35
+ }
36
+ return deleted
37
+ }
38
+
39
+ async function resolveIdentifierKeys(cache: CacheStrategy, ids: string[]): Promise<string[]> {
40
+ const matches = new Set<string>()
41
+ for (const id of normalizeUnique(ids)) {
42
+ const keys = await cache.keys(`*${id}*`)
43
+ for (const key of keys) matches.add(key)
44
+ }
45
+ return Array.from(matches).sort((a, b) => a.localeCompare(b))
46
+ }
47
+
48
+ export async function collectCacheStats(cache: CacheStrategy) {
49
+ const stats = await collectCrudCacheStats(cache)
50
+ return {
51
+ generatedAt: stats.generatedAt,
52
+ totalKeys: stats.totalKeys,
53
+ segments: stats.segments.map((segment) => ({
54
+ segment: segment.segment,
55
+ keyCount: segment.keyCount,
56
+ method: segment.method,
57
+ path: segment.path,
58
+ resource: segment.resource,
59
+ })),
60
+ }
61
+ }
62
+
63
+ export async function previewCachePurge(
64
+ cache: CacheStrategy,
65
+ request: CachePurgeRequest,
66
+ ): Promise<CachePurgeResult> {
67
+ if (request.kind === 'all') {
68
+ const keys = (await cache.keys()).sort((a, b) => a.localeCompare(b))
69
+ return { deleted: keys.length, keys, note: null }
70
+ }
71
+
72
+ if (request.kind === 'segment') {
73
+ const stats = await collectCrudCacheStats(cache)
74
+ const target = stats.segments.find((segment) => segment.segment === request.segment)
75
+ return {
76
+ deleted: target?.keys.length ?? 0,
77
+ keys: (target?.keys ?? []).slice().sort((a, b) => a.localeCompare(b)),
78
+ note: target ? null : `Cache segment "${request.segment}" was not found in this scope.`,
79
+ }
80
+ }
81
+
82
+ if (request.kind === 'tags') {
83
+ return {
84
+ deleted: 0,
85
+ keys: [],
86
+ note: 'Tag purges do not expose matching keys through the cache interface. Run without `--dry-run` to execute.',
87
+ }
88
+ }
89
+
90
+ if (request.kind === 'keys') {
91
+ const requested = normalizeUnique(request.keys)
92
+ const existingKeys = await cache.keys()
93
+ const existingSet = new Set(existingKeys)
94
+ const keys = requested.filter((key) => existingSet.has(key))
95
+ return {
96
+ deleted: keys.length,
97
+ keys,
98
+ note: keys.length === requested.length ? null : 'Some requested keys were not present in this scope.',
99
+ }
100
+ }
101
+
102
+ if (request.kind === 'ids') {
103
+ const keys = await resolveIdentifierKeys(cache, request.ids)
104
+ return {
105
+ deleted: keys.length,
106
+ keys,
107
+ note: keys.length > 0 ? null : 'No cache keys matched the requested identifier tokens.',
108
+ }
109
+ }
110
+
111
+ const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b))
112
+ return {
113
+ deleted: keys.length,
114
+ keys,
115
+ note: keys.length > 0 ? null : `No cache keys matched pattern "${request.pattern}".`,
116
+ }
117
+ }
118
+
119
+ export async function executeCachePurge(
120
+ cache: CacheStrategy,
121
+ request: CachePurgeRequest,
122
+ ): Promise<CachePurgeResult> {
123
+ if (request.kind === 'all') {
124
+ const keys = (await cache.keys()).sort((a, b) => a.localeCompare(b))
125
+ const deleted = await cache.clear()
126
+ return { deleted, keys, note: null }
127
+ }
128
+
129
+ if (request.kind === 'segment') {
130
+ const result = await purgeCrudCacheSegment(cache, request.segment)
131
+ return {
132
+ deleted: result.deleted,
133
+ keys: result.keys.slice().sort((a, b) => a.localeCompare(b)),
134
+ note: result.keys.length > 0 ? null : `Cache segment "${request.segment}" was not found in this scope.`,
135
+ }
136
+ }
137
+
138
+ if (request.kind === 'tags') {
139
+ const deleted = await cache.deleteByTags(normalizeUnique(request.tags))
140
+ return {
141
+ deleted,
142
+ keys: [],
143
+ note: 'Tag purges report counts only because the cache interface does not expose tag-to-key listings.',
144
+ }
145
+ }
146
+
147
+ if (request.kind === 'keys') {
148
+ const preview = await previewCachePurge(cache, request)
149
+ const deleted = await deleteKeys(cache, preview.keys)
150
+ return {
151
+ deleted,
152
+ keys: preview.keys,
153
+ note: preview.note,
154
+ }
155
+ }
156
+
157
+ if (request.kind === 'ids') {
158
+ const keys = await resolveIdentifierKeys(cache, request.ids)
159
+ const deleted = await deleteKeys(cache, keys)
160
+ return {
161
+ deleted,
162
+ keys,
163
+ note: keys.length > 0 ? null : 'No cache keys matched the requested identifier tokens.',
164
+ }
165
+ }
166
+
167
+ const keys = (await cache.keys(request.pattern)).sort((a, b) => a.localeCompare(b))
168
+ const deleted = await deleteKeys(cache, keys)
169
+ return {
170
+ deleted,
171
+ keys,
172
+ note: keys.length > 0 ? null : `No cache keys matched pattern "${request.pattern}".`,
173
+ }
174
+ }
@@ -211,6 +211,7 @@
211
211
  "customers.companies.form.submit": "Unternehmen erstellen",
212
212
  "customers.companies.form.success": "Unternehmen erstellt",
213
213
  "customers.companies.form.updateSuccess": "Unternehmen aktualisiert.",
214
+ "customers.companies.list.actions.bulkDelete": "Auswahl löschen",
214
215
  "customers.companies.list.actions.delete": "Löschen",
215
216
  "customers.companies.list.actions.new": "Neues Unternehmen",
216
217
  "customers.companies.list.actions.openInNewTab": "In neuem Tab öffnen",
@@ -488,6 +489,7 @@
488
489
  "customers.errors.company_required": "Unternehmens-ID ist erforderlich",
489
490
  "customers.errors.deal_required": "Deal-ID ist erforderlich",
490
491
  "customers.errors.interaction_required": "Interaction-ID ist erforderlich",
492
+ "customers.errors.internalError": "Interner Serverfehler",
491
493
  "customers.errors.invalid_query": "Ungültige Abfrageparameter",
492
494
  "customers.errors.lookup_failed": "Lexikon konnte nicht geladen werden",
493
495
  "customers.errors.organization_forbidden": "Organisation nicht verfügbar",
@@ -506,6 +508,7 @@
506
508
  "customers.errors.todo_unlink_failed": "To-do-Verknüpfung konnte nicht gelöst werden",
507
509
  "customers.errors.unassign_failed": "Tag konnte nicht entfernt werden",
508
510
  "customers.errors.unauthorized": "Nicht autorisiert",
511
+ "customers.errors.validationFailed": "Validierung fehlgeschlagen",
509
512
  "customers.interactions.cancel.confirm": "Cancel this interaction?",
510
513
  "customers.interactions.cancel.error": "Failed to cancel interaction.",
511
514
  "customers.interactions.cancel.success": "Interaction canceled.",
@@ -720,7 +723,9 @@
720
723
  "customers.people.detail.fields.jobTitle": "Position",
721
724
  "customers.people.detail.fields.lifecycleStage": "Lebenszyklusphase",
722
725
  "customers.people.detail.fields.linkedIn": "LinkedIn",
726
+ "customers.people.detail.fields.seniority": "Senioritätsstufe",
723
727
  "customers.people.detail.fields.source": "Quelle",
728
+ "customers.people.detail.fields.timezone": "Zeitzone",
724
729
  "customers.people.detail.fields.twitter": "Twitter",
725
730
  "customers.people.detail.highlights.company": "Unternehmen",
726
731
  "customers.people.detail.highlights.nextInteraction": "Nächste Interaktion",
@@ -964,6 +969,7 @@
964
969
  "customers.people.form.phoneChecking": "Prüfe auf bestehende Telefonnummern…",
965
970
  "customers.people.form.phoneDuplicateLink": "Datensatz öffnen",
966
971
  "customers.people.form.phoneDuplicateNotice": "Diese Telefonnummer ist bereits {{name}} zugeordnet.",
972
+ "customers.people.form.preferredName": "Bevorzugter Name",
967
973
  "customers.people.form.primaryEmail": "E-Mail",
968
974
  "customers.people.form.primaryEmailPlaceholder": "name@beispiel.de",
969
975
  "customers.people.form.primaryPhone": "Telefon",
@@ -211,6 +211,7 @@
211
211
  "customers.companies.form.submit": "Create Company",
212
212
  "customers.companies.form.success": "Company created",
213
213
  "customers.companies.form.updateSuccess": "Company updated.",
214
+ "customers.companies.list.actions.bulkDelete": "Delete selected",
214
215
  "customers.companies.list.actions.delete": "Delete",
215
216
  "customers.companies.list.actions.new": "New Company",
216
217
  "customers.companies.list.actions.openInNewTab": "Open in new tab",
@@ -488,6 +489,7 @@
488
489
  "customers.errors.company_required": "Company id is required",
489
490
  "customers.errors.deal_required": "Deal id is required",
490
491
  "customers.errors.interaction_required": "Interaction id is required",
492
+ "customers.errors.internalError": "Internal server error",
491
493
  "customers.errors.invalid_query": "Invalid query parameters",
492
494
  "customers.errors.lookup_failed": "Failed to load dictionary entries",
493
495
  "customers.errors.organization_forbidden": "Organization not accessible",
@@ -506,6 +508,7 @@
506
508
  "customers.errors.todo_unlink_failed": "Failed to unlink todo",
507
509
  "customers.errors.unassign_failed": "Failed to unassign tag",
508
510
  "customers.errors.unauthorized": "Unauthorized",
511
+ "customers.errors.validationFailed": "Validation failed",
509
512
  "customers.interactions.cancel.confirm": "Cancel this interaction?",
510
513
  "customers.interactions.cancel.error": "Failed to cancel interaction.",
511
514
  "customers.interactions.cancel.success": "Interaction canceled.",
@@ -720,7 +723,9 @@
720
723
  "customers.people.detail.fields.jobTitle": "Job title",
721
724
  "customers.people.detail.fields.lifecycleStage": "Lifecycle stage",
722
725
  "customers.people.detail.fields.linkedIn": "LinkedIn",
726
+ "customers.people.detail.fields.seniority": "Seniority",
723
727
  "customers.people.detail.fields.source": "Source",
728
+ "customers.people.detail.fields.timezone": "Timezone",
724
729
  "customers.people.detail.fields.twitter": "Twitter",
725
730
  "customers.people.detail.highlights.company": "Company",
726
731
  "customers.people.detail.highlights.nextInteraction": "Next interaction",
@@ -964,6 +969,7 @@
964
969
  "customers.people.form.phoneChecking": "Checking for matching phone numbers…",
965
970
  "customers.people.form.phoneDuplicateLink": "Open record",
966
971
  "customers.people.form.phoneDuplicateNotice": "This phone number is already assigned to {{name}}.",
972
+ "customers.people.form.preferredName": "Preferred name",
967
973
  "customers.people.form.primaryEmail": "Primary email",
968
974
  "customers.people.form.primaryEmailPlaceholder": "name@example.com",
969
975
  "customers.people.form.primaryPhone": "Primary phone",
@@ -211,6 +211,7 @@
211
211
  "customers.companies.form.submit": "Crear empresa",
212
212
  "customers.companies.form.success": "Empresa creada",
213
213
  "customers.companies.form.updateSuccess": "Empresa actualizada.",
214
+ "customers.companies.list.actions.bulkDelete": "Eliminar seleccionados",
214
215
  "customers.companies.list.actions.delete": "Eliminar",
215
216
  "customers.companies.list.actions.new": "Nueva empresa",
216
217
  "customers.companies.list.actions.openInNewTab": "Abrir en nueva pestaña",
@@ -488,6 +489,7 @@
488
489
  "customers.errors.company_required": "Se requiere el ID de la empresa",
489
490
  "customers.errors.deal_required": "Se requiere el ID de la oportunidad",
490
491
  "customers.errors.interaction_required": "Se requiere el ID de la interaccion",
492
+ "customers.errors.internalError": "Error interno del servidor",
491
493
  "customers.errors.invalid_query": "Parámetros de consulta no válidos",
492
494
  "customers.errors.lookup_failed": "No se pudo cargar el diccionario",
493
495
  "customers.errors.organization_forbidden": "Organización no accesible",
@@ -506,6 +508,7 @@
506
508
  "customers.errors.todo_unlink_failed": "No se pudo desvincular la tarea",
507
509
  "customers.errors.unassign_failed": "No se pudo quitar la etiqueta",
508
510
  "customers.errors.unauthorized": "No autorizado",
511
+ "customers.errors.validationFailed": "La validación falló",
509
512
  "customers.interactions.cancel.confirm": "Cancel this interaction?",
510
513
  "customers.interactions.cancel.error": "Failed to cancel interaction.",
511
514
  "customers.interactions.cancel.success": "Interaction canceled.",
@@ -720,7 +723,9 @@
720
723
  "customers.people.detail.fields.jobTitle": "Cargo",
721
724
  "customers.people.detail.fields.lifecycleStage": "Etapa",
722
725
  "customers.people.detail.fields.linkedIn": "LinkedIn",
726
+ "customers.people.detail.fields.seniority": "Seniority",
723
727
  "customers.people.detail.fields.source": "Origen",
728
+ "customers.people.detail.fields.timezone": "Zona horaria",
724
729
  "customers.people.detail.fields.twitter": "Twitter",
725
730
  "customers.people.detail.highlights.company": "Empresa",
726
731
  "customers.people.detail.highlights.nextInteraction": "Próxima interacción",
@@ -964,6 +969,7 @@
964
969
  "customers.people.form.phoneChecking": "Comprobando si el número de teléfono ya existe…",
965
970
  "customers.people.form.phoneDuplicateLink": "Abrir registro",
966
971
  "customers.people.form.phoneDuplicateNotice": "Este número de teléfono ya está asignado a {{name}}.",
972
+ "customers.people.form.preferredName": "Nombre preferido",
967
973
  "customers.people.form.primaryEmail": "Correo electrónico",
968
974
  "customers.people.form.primaryEmailPlaceholder": "nombre@ejemplo.com",
969
975
  "customers.people.form.primaryPhone": "Teléfono",
@@ -211,6 +211,7 @@
211
211
  "customers.companies.form.submit": "Utwórz firmę",
212
212
  "customers.companies.form.success": "Firma została utworzona",
213
213
  "customers.companies.form.updateSuccess": "Firma została zaktualizowana.",
214
+ "customers.companies.list.actions.bulkDelete": "Usuń zaznaczone",
214
215
  "customers.companies.list.actions.delete": "Usuń",
215
216
  "customers.companies.list.actions.new": "Nowa firma",
216
217
  "customers.companies.list.actions.openInNewTab": "Otwórz w nowej karcie",
@@ -488,6 +489,7 @@
488
489
  "customers.errors.company_required": "Wymagany identyfikator firmy",
489
490
  "customers.errors.deal_required": "Wymagany identyfikator szansy",
490
491
  "customers.errors.interaction_required": "Wymagany identyfikator interakcji",
492
+ "customers.errors.internalError": "Wewnętrzny błąd serwera",
491
493
  "customers.errors.invalid_query": "Nieprawidłowe parametry zapytania",
492
494
  "customers.errors.lookup_failed": "Nie udało się załadować słownika",
493
495
  "customers.errors.organization_forbidden": "Organizacja jest niedostępna",
@@ -506,6 +508,7 @@
506
508
  "customers.errors.todo_unlink_failed": "Nie udało się odpiąć zadania",
507
509
  "customers.errors.unassign_failed": "Nie udało się odpiąć tagu",
508
510
  "customers.errors.unauthorized": "Brak autoryzacji",
511
+ "customers.errors.validationFailed": "Walidacja nie powiodła się",
509
512
  "customers.interactions.cancel.confirm": "Cancel this interaction?",
510
513
  "customers.interactions.cancel.error": "Failed to cancel interaction.",
511
514
  "customers.interactions.cancel.success": "Interaction canceled.",
@@ -720,7 +723,9 @@
720
723
  "customers.people.detail.fields.jobTitle": "Stanowisko",
721
724
  "customers.people.detail.fields.lifecycleStage": "Etap cyklu",
722
725
  "customers.people.detail.fields.linkedIn": "LinkedIn",
726
+ "customers.people.detail.fields.seniority": "Poziom seniority",
723
727
  "customers.people.detail.fields.source": "Źródło",
728
+ "customers.people.detail.fields.timezone": "Strefa czasowa",
724
729
  "customers.people.detail.fields.twitter": "Twitter",
725
730
  "customers.people.detail.highlights.company": "Firma",
726
731
  "customers.people.detail.highlights.nextInteraction": "Następna interakcja",
@@ -964,6 +969,7 @@
964
969
  "customers.people.form.phoneChecking": "Sprawdzanie, czy numer telefonu jest już używany…",
965
970
  "customers.people.form.phoneDuplicateLink": "Otwórz rekord",
966
971
  "customers.people.form.phoneDuplicateNotice": "Ten numer telefonu jest już przypisany do {{name}}.",
972
+ "customers.people.form.preferredName": "Preferowane imię",
967
973
  "customers.people.form.primaryEmail": "E-mail",
968
974
  "customers.people.form.primaryEmailPlaceholder": "nazwa@example.com",
969
975
  "customers.people.form.primaryPhone": "Telefon",