@open-mercato/shared 0.6.8-develop.6983.1.e4f794099e → 0.6.8-develop.6985.1.fb93574faa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/lib/delivery/retry.js +11 -0
- package/dist/lib/delivery/retry.js.map +7 -0
- package/dist/lib/query/advanced-filter.js +5 -1
- package/dist/lib/query/advanced-filter.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/delivery/__tests__/retry.test.ts +54 -0
- package/src/lib/delivery/retry.ts +23 -0
- package/src/lib/query/__tests__/advanced-filter.test.ts +36 -0
- package/src/lib/query/advanced-filter.ts +14 -1
- package/src/modules/notifications/types.ts +82 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 260 entry points
|
|
2
2
|
[build:shared] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -45,6 +45,7 @@ yarn workspace @open-mercato/shared build
|
|
|
45
45
|
| `custom-fields/` | When handling custom field payloads | `@open-mercato/shared/lib/custom-fields` |
|
|
46
46
|
| `data/` | When you need `DataEngine` or `QueryEngine` types | `@open-mercato/shared/lib/data/engine` |
|
|
47
47
|
| `db/` | When resolving the ORM/connection-pool config (`resolvePoolConfig`, pool/timeout env knobs) | `@open-mercato/shared/lib/db/mikro` |
|
|
48
|
+
| `delivery/` | When scheduling delivery/retry attempts — exponential backoff with jitter for delivery pipelines (currently the push delivery worker) | `@open-mercato/shared/lib/delivery/retry` (`calculateBackoffDelayMs`) |
|
|
48
49
|
| `di/` | When setting up dependency injection (Awilix). The app-level hook (`src/di.ts` → `register`) is wired explicitly in BOTH bootstrap paths — `src/bootstrap.ts` for the Next.js runtime, `bootstrapFromAppRoot()` for worker/scheduler/CLI processes. Never rely on the legacy `import('@/di')` fallback: the alias does not exist outside the app bundler | `@open-mercato/shared/lib/di` |
|
|
49
50
|
| `encryption/` | When querying encrypted entities (MUST use instead of raw `em.find`) | `@open-mercato/shared/lib/encryption/find` |
|
|
50
51
|
| `i18n/` | When translating strings — `useT()` client-side, `resolveTranslations()` server-side | `@open-mercato/shared/lib/i18n/context` or `/server` |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function calculateBackoffDelayMs(attemptNumber, options = {}) {
|
|
2
|
+
const baseDelayMs = options.baseDelayMs ?? 1e3;
|
|
3
|
+
const maxJitterMs = options.maxJitterMs ?? 1e3;
|
|
4
|
+
const factor = options.factor ?? 2;
|
|
5
|
+
const jitterMs = maxJitterMs > 0 ? Math.floor(Math.random() * maxJitterMs) : 0;
|
|
6
|
+
return baseDelayMs * Math.pow(factor, Math.max(attemptNumber - 1, 0)) + jitterMs;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
calculateBackoffDelayMs
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/delivery/retry.ts"],
|
|
4
|
+
"sourcesContent": ["export type BackoffOptions = {\n baseDelayMs?: number\n maxJitterMs?: number\n factor?: number\n}\n\n/**\n * Exponential backoff with jitter for delivery-retry scheduling.\n *\n * `delay = baseDelayMs * factor^(attemptNumber - 1) + random(0, maxJitterMs)`\n *\n * `attemptNumber` is 1-based (the number of the attempt that just failed). The jitter spreads\n * simultaneous retries so a provider outage does not make every failed delivery re-fire at the\n * same instant (thundering herd). Reusable across delivery pipelines that need identical backoff\n * semantics instead of hand-rolling their own (currently the push delivery worker).\n */\nexport function calculateBackoffDelayMs(attemptNumber: number, options: BackoffOptions = {}): number {\n const baseDelayMs = options.baseDelayMs ?? 1000\n const maxJitterMs = options.maxJitterMs ?? 1000\n const factor = options.factor ?? 2\n const jitterMs = maxJitterMs > 0 ? Math.floor(Math.random() * maxJitterMs) : 0\n return baseDelayMs * Math.pow(factor, Math.max(attemptNumber - 1, 0)) + jitterMs\n}\n"],
|
|
5
|
+
"mappings": "AAgBO,SAAS,wBAAwB,eAAuB,UAA0B,CAAC,GAAW;AACnG,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,WAAW,cAAc,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,WAAW,IAAI;AAC7E,SAAO,cAAc,KAAK,IAAI,QAAQ,KAAK,IAAI,gBAAgB,GAAG,CAAC,CAAC,IAAI;AAC1E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -106,6 +106,9 @@ function serializeAdvancedFilter(state) {
|
|
|
106
106
|
});
|
|
107
107
|
return params;
|
|
108
108
|
}
|
|
109
|
+
function isUsableFilterField(field) {
|
|
110
|
+
return field.length > 0 && !field.startsWith("$");
|
|
111
|
+
}
|
|
109
112
|
function deserializeAdvancedFilter(query) {
|
|
110
113
|
const logic = normalizeJoinOperator(query["filter[logic]"]);
|
|
111
114
|
const conditions = [];
|
|
@@ -113,7 +116,7 @@ function deserializeAdvancedFilter(query) {
|
|
|
113
116
|
const field = query[`filter[conditions][${i}][field]`];
|
|
114
117
|
const op = query[`filter[conditions][${i}][op]`];
|
|
115
118
|
if (typeof field !== "string" || typeof op !== "string") break;
|
|
116
|
-
if (!isValidOperator(op)) continue;
|
|
119
|
+
if (!isValidOperator(op) || !isUsableFilterField(field)) continue;
|
|
117
120
|
const value = query[`filter[conditions][${i}][value]`];
|
|
118
121
|
const join = i === 0 ? "and" : normalizeJoinOperator(query[`filter[conditions][${i}][join]`], logic);
|
|
119
122
|
conditions.push({
|
|
@@ -304,6 +307,7 @@ function readTreeGroup(prefix, query) {
|
|
|
304
307
|
const field = query[`${childPrefix}[field]`];
|
|
305
308
|
const op = query[`${childPrefix}[op]`];
|
|
306
309
|
if (typeof field !== "string" || typeof op !== "string" || !isValidOperator(op)) continue;
|
|
310
|
+
if (!isUsableFilterField(field)) continue;
|
|
307
311
|
const rawVal = query[`${childPrefix}[value]`];
|
|
308
312
|
children.push({
|
|
309
313
|
id: crypto.randomUUID(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/query/advanced-filter.ts"],
|
|
4
|
-
"sourcesContent": ["import { buildIlikeTerm } from '../db/buildIlikeTerm'\n\nexport type FilterOperator =\n | 'is' | 'is_not' | 'contains' | 'does_not_contain' | 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty'\n | 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'between'\n | 'is_before' | 'is_after'\n | 'is_any_of' | 'is_none_of'\n | 'is_true' | 'is_false'\n | 'has_any_of' | 'has_all_of' | 'has_none_of'\n\nexport type FilterFieldType = 'text' | 'number' | 'date' | 'select' | 'boolean' | 'tags'\n\nexport type FilterOptionTone = 'success' | 'error' | 'warning' | 'info' | 'neutral' | 'brand' | 'pink'\n\nexport type FilterOption = {\n value: string\n label: string\n tone?: FilterOptionTone\n}\n\nexport type FilterFieldDef = {\n key: string\n label: string\n type: FilterFieldType\n group?: string\n iconName?: string\n loadOptions?: (query?: string) => Promise<FilterOption[]>\n options?: FilterOption[]\n}\n\nexport type FilterJoinOperator = 'and' | 'or'\n\nexport type FilterCondition = {\n id: string\n field: string\n operator: FilterOperator\n value: unknown\n join?: FilterJoinOperator\n}\n\nexport type AdvancedFilterState = {\n logic: FilterJoinOperator\n conditions: FilterCondition[]\n}\n\nexport const OPERATORS_BY_FIELD_TYPE: Record<FilterFieldType, FilterOperator[]> = {\n text: ['is', 'is_not', 'contains', 'does_not_contain', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty'],\n number: ['equals', 'not_equals', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'between', 'is_empty'],\n date: ['is', 'is_before', 'is_after', 'between', 'is_empty', 'is_not_empty'],\n select: ['is', 'is_not', 'is_any_of', 'is_none_of', 'is_empty'],\n boolean: ['is_true', 'is_false'],\n tags: ['has_any_of', 'has_all_of', 'has_none_of', 'is_empty'],\n}\n\nexport function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {\n switch (fieldType) {\n case 'text': return 'contains'\n case 'number': return 'equals'\n case 'date': return 'is_after'\n case 'select': return 'is'\n case 'boolean': return 'is_true'\n case 'tags': return 'has_any_of'\n }\n}\n\nconst VALID_OPERATORS = new Set<string>([\n 'is', 'is_not', 'contains', 'does_not_contain', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty',\n 'equals', 'not_equals', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'between',\n 'is_before', 'is_after',\n 'is_any_of', 'is_none_of',\n 'is_true', 'is_false',\n 'has_any_of', 'has_all_of', 'has_none_of',\n])\n\nexport function isValidOperator(op: string): op is FilterOperator {\n return VALID_OPERATORS.has(op)\n}\n\nexport function isValuelessOperator(operator: FilterOperator): boolean {\n return operator === 'is_empty' || operator === 'is_not_empty' || operator === 'is_true' || operator === 'is_false'\n}\n\nexport function createEmptyCondition(): FilterCondition {\n return {\n id: crypto.randomUUID(),\n field: '',\n operator: 'contains',\n value: '',\n join: 'and',\n }\n}\n\nfunction normalizeJoinOperator(value: unknown, fallback: FilterJoinOperator = 'and'): FilterJoinOperator {\n return value === 'or' ? 'or' : value === 'and' ? 'and' : fallback\n}\n\nfunction parseSerializedFilterValue(value: unknown): unknown {\n if (typeof value !== 'string') return value ?? null\n const trimmed = value.trim()\n if (!trimmed) return value\n if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return value\n try {\n return JSON.parse(trimmed)\n } catch {\n return value\n }\n}\n\nexport function normalizeAdvancedFilterState(state: AdvancedFilterState): AdvancedFilterState {\n const logic = normalizeJoinOperator(state.logic)\n return {\n logic,\n conditions: state.conditions.map((condition, index) => ({\n ...condition,\n join: index === 0 ? 'and' : normalizeJoinOperator(condition.join, logic),\n })),\n }\n}\n\nexport function serializeAdvancedFilter(state: AdvancedFilterState): Record<string, string> {\n const params: Record<string, string> = {}\n const normalized = normalizeAdvancedFilterState(state)\n if (!normalized.conditions.length) return params\n params['filter[logic]'] = normalized.logic\n normalized.conditions.forEach((condition, index) => {\n const prefix = `filter[conditions][${index}]`\n params[`${prefix}[field]`] = condition.field\n params[`${prefix}[op]`] = condition.operator\n if (index > 0) {\n params[`${prefix}[join]`] = condition.join ?? normalized.logic\n }\n if (!isValuelessOperator(condition.operator) && condition.value != null) {\n params[`${prefix}[value]`] = typeof condition.value === 'object'\n ? JSON.stringify(condition.value)\n : String(condition.value)\n }\n })\n return params\n}\n\nexport function deserializeAdvancedFilter(query: Record<string, unknown>): AdvancedFilterState | null {\n const logic = normalizeJoinOperator(query['filter[logic]'])\n const conditions: FilterCondition[] = []\n for (let i = 0; i < 20; i++) {\n const field = query[`filter[conditions][${i}][field]`]\n const op = query[`filter[conditions][${i}][op]`]\n if (typeof field !== 'string' || typeof op !== 'string') break\n if (!isValidOperator(op)) continue\n const value = query[`filter[conditions][${i}][value]`]\n const join = i === 0\n ? 'and'\n : normalizeJoinOperator(query[`filter[conditions][${i}][join]`], logic)\n conditions.push({\n id: String(i),\n field,\n operator: op,\n value: parseSerializedFilterValue(value),\n join,\n })\n }\n\n if (!conditions.length) return null\n return normalizeAdvancedFilterState({ logic, conditions })\n}\n\nfunction buildConditionFilter(condition: FilterCondition): Record<string, unknown> | null {\n if (!condition.field || !condition.operator) return null\n const normalizeSingleValue = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n const normalizeListValue = (value: unknown): unknown[] => {\n const list = Array.isArray(value) ? value : [value]\n return list\n .map((entry) => normalizeSingleValue(entry))\n .filter((entry) => entry !== null)\n }\n const filter: Record<string, unknown> = {}\n switch (condition.operator) {\n case 'is':\n case 'equals':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $eq: normalizeSingleValue(condition.value) }\n break\n case 'is_not':\n case 'not_equals':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ne: normalizeSingleValue(condition.value) }\n break\n case 'contains':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value))) }\n break\n case 'does_not_contain':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $not: { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value))) } }\n break\n case 'starts_with':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value)), 'startsWith') }\n break\n case 'ends_with':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value)), 'endsWith') }\n break\n case 'is_empty':\n filter[condition.field] = { $exists: false }\n break\n case 'is_not_empty':\n filter[condition.field] = { $exists: true }\n break\n case 'greater_than':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gt: normalizeSingleValue(condition.value) }\n break\n case 'less_than':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lt: normalizeSingleValue(condition.value) }\n break\n case 'greater_or_equal':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gte: normalizeSingleValue(condition.value) }\n break\n case 'less_or_equal':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lte: normalizeSingleValue(condition.value) }\n break\n case 'between':\n if (Array.isArray(condition.value) && condition.value.length === 2) {\n const start = normalizeSingleValue(condition.value[0])\n const end = normalizeSingleValue(condition.value[1])\n if (start === null && end === null) return null\n if (start !== null && end !== null) {\n filter[condition.field] = { $gte: start, $lte: end }\n } else if (start !== null) {\n filter[condition.field] = { $gte: start }\n } else if (end !== null) {\n filter[condition.field] = { $lte: end }\n }\n }\n break\n case 'is_before':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lt: normalizeSingleValue(condition.value) }\n break\n case 'is_after':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gt: normalizeSingleValue(condition.value) }\n break\n case 'is_any_of':\n case 'has_any_of':\n if (normalizeListValue(condition.value).length === 0) return null\n filter[condition.field] = { $in: normalizeListValue(condition.value) }\n break\n case 'is_none_of':\n case 'has_none_of':\n if (normalizeListValue(condition.value).length === 0) return null\n filter[condition.field] = { $nin: normalizeListValue(condition.value) }\n break\n case 'has_all_of': {\n const allOfValues = normalizeListValue(condition.value)\n if (allOfValues.length === 0) return null\n filter[condition.field] = { $contains: allOfValues }\n break\n }\n case 'is_true':\n filter[condition.field] = { $eq: true }\n break\n case 'is_false':\n filter[condition.field] = { $eq: false }\n break\n }\n return Object.keys(filter).length > 0 ? filter : null\n}\n\nexport function convertAdvancedFilterToWhere(state: AdvancedFilterState): Record<string, unknown> {\n const normalized = normalizeAdvancedFilterState(state)\n if (!normalized.conditions.length) return {}\n\n const conditionEntries = normalized.conditions\n .map((condition) => {\n const filter = buildConditionFilter(condition)\n return filter\n ? {\n join: normalizeJoinOperator(condition.join, normalized.logic),\n filter,\n }\n : null\n })\n .filter((entry): entry is { join: FilterJoinOperator; filter: Record<string, unknown> } => entry !== null)\n\n if (!conditionEntries.length) return {}\n\n let clauses: Record<string, unknown>[] = [conditionEntries[0].filter]\n for (const entry of conditionEntries.slice(1)) {\n if (entry.join === 'or') {\n clauses = [...clauses, entry.filter]\n continue\n }\n clauses = clauses.map((clause) => ({\n ...clause,\n ...entry.filter,\n }))\n }\n\n return clauses.length > 1 ? { $or: clauses } : clauses[0]\n}\n\n// -----------------------------------------------------------------------------\n// v2 tree serialization (advanced-filter-tree)\n// -----------------------------------------------------------------------------\n\nimport type {\n AdvancedFilterTree,\n FilterRule as TreeFilterRule,\n FilterGroup as TreeFilterGroup,\n FilterCombinator as TreeFilterCombinator,\n} from './advanced-filter-tree'\n\nexport function serializeTree(tree: AdvancedFilterTree): Record<string, string> {\n if (!treeHasRules(tree.root)) return {}\n const out: Record<string, string> = { 'filter[v]': '2' }\n serializeTreeGroup(tree.root, 'filter[root]', out)\n return out\n}\n\nfunction treeHasRules(group: TreeFilterGroup): boolean {\n return group.children.some((child) => child.type === 'rule' || treeHasRules(child))\n}\n\nfunction serializeTreeGroup(group: TreeFilterGroup, prefix: string, out: Record<string, string>): void {\n out[`${prefix}[combinator]`] = group.combinator\n group.children.forEach((child, idx) => {\n const childPrefix = `${prefix}[children][${idx}]`\n if (child.type === 'rule') serializeTreeRule(child, childPrefix, out)\n else { out[`${childPrefix}[type]`] = 'group'; serializeTreeGroup(child, childPrefix, out) }\n })\n}\n\nfunction serializeTreeRule(rule: TreeFilterRule, prefix: string, out: Record<string, string>): void {\n out[`${prefix}[type]`] = 'rule'\n out[`${prefix}[field]`] = rule.field\n out[`${prefix}[op]`] = rule.operator\n if (!isValuelessOperator(rule.operator) && rule.value != null) {\n out[`${prefix}[value]`] = typeof rule.value === 'object'\n ? JSON.stringify(rule.value)\n : String(rule.value)\n }\n}\n\nexport function deserializeTree(query: Record<string, unknown>): AdvancedFilterTree | null {\n if (query['filter[v]'] !== '2') return null\n const root = readTreeGroup('filter[root]', query)\n if (!root) return null\n return { root }\n}\n\nfunction readTreeGroup(prefix: string, query: Record<string, unknown>): TreeFilterGroup | null {\n const combRaw = query[`${prefix}[combinator]`]\n if (combRaw !== 'and' && combRaw !== 'or') return null\n const children: Array<TreeFilterRule | TreeFilterGroup> = []\n for (let i = 0; i < 64; i++) {\n const childPrefix = `${prefix}[children][${i}]`\n const type = query[`${childPrefix}[type]`]\n if (type === 'rule') {\n const field = query[`${childPrefix}[field]`]\n const op = query[`${childPrefix}[op]`]\n if (typeof field !== 'string' || typeof op !== 'string' || !isValidOperator(op)) continue\n const rawVal = query[`${childPrefix}[value]`]\n children.push({\n id: crypto.randomUUID(),\n type: 'rule',\n field,\n operator: op as FilterOperator,\n value: parseSerializedFilterValue(rawVal),\n })\n } else if (type === 'group') {\n const sub = readTreeGroup(childPrefix, query)\n if (sub) children.push(sub)\n } else {\n break\n }\n }\n return {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: combRaw as TreeFilterCombinator,\n children,\n }\n}\n\n/**\n * Runtime discriminator: is this value an `AdvancedFilterState` (legacy flat\n * shape with `logic` + `conditions`) rather than an `AdvancedFilterTree`\n * (`{root: FilterGroup}`)?\n *\n * Used at the DataTable boundary to bridge legacy callers onto the new tree\n * model without forcing third-party module developers to migrate immediately.\n * See `BACKWARD_COMPATIBILITY.md` \u00A73 and the spec's \"Migration & Backward\n * Compatibility\" section.\n */\nexport function isAdvancedFilterState(value: unknown): value is AdvancedFilterState {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n return Array.isArray(record.conditions) && (record.logic === 'and' || record.logic === 'or')\n}\n\n/**\n * Convert legacy flat AdvancedFilterState into a tree under standard SQL precedence\n * (AND binds tighter than OR). Runs of consecutive AND-joined conditions become\n * AND-subgroups, and the OR connectors join those subgroups in a root OR-group.\n */\nexport function flatToTree(flat: AdvancedFilterState): AdvancedFilterTree {\n if (flat.conditions.length === 0) {\n return { root: { id: crypto.randomUUID(), type: 'group', combinator: 'and', children: [] } }\n }\n\n // Step A: split into AND-runs separated by OR connectors. The first row's `join`\n // is logically \"and\" (no left neighbor); we never use it as a separator.\n const andRuns: FilterCondition[][] = [[flat.conditions[0]]]\n for (let i = 1; i < flat.conditions.length; i++) {\n const c = flat.conditions[i]\n if (c.join === 'or') andRuns.push([c])\n else andRuns[andRuns.length - 1].push(c)\n }\n\n const ruleFromCondition = (c: FilterCondition): TreeFilterRule => ({\n id: crypto.randomUUID(),\n type: 'rule',\n field: c.field,\n operator: c.operator,\n value: c.value,\n })\n\n // Step B: each AND-run becomes either a rule (length 1) or an AND-group.\n const orChildren: Array<TreeFilterRule | TreeFilterGroup> = andRuns.map((run) => {\n if (run.length === 1) return ruleFromCondition(run[0])\n return {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: 'and',\n children: run.map(ruleFromCondition),\n }\n })\n\n // Step C: zero or one OR-disjunct -> root combinator stays \"and\"; otherwise OR.\n if (orChildren.length === 1) {\n const only = orChildren[0]\n if (only.type === 'group') return { root: only }\n return { root: { id: crypto.randomUUID(), type: 'group', combinator: 'and', children: [only] } }\n }\n return {\n root: { id: crypto.randomUUID(), type: 'group', combinator: 'or', children: orChildren },\n }\n}\n\n/**\n * Map a dictionary entry's display color (named like 'red'/'green' or a hex like '#ef4444')\n * to a `FilterOptionTone`, so dictionary-backed select options can render with the\n * correct status dot in chips and value pills. Unknown / undefined inputs return\n * `undefined` so the consumer falls back to plain (no tone) rendering.\n */\nexport function mapDictionaryColorToTone(color: string | null | undefined): FilterOptionTone | undefined {\n if (!color || typeof color !== 'string') return undefined\n const trimmed = color.trim().toLowerCase()\n if (!trimmed) return undefined\n const named: Record<string, FilterOptionTone> = {\n red: 'error',\n crimson: 'error',\n pink: 'pink',\n rose: 'pink',\n fuchsia: 'pink',\n magenta: 'pink',\n green: 'success',\n emerald: 'success',\n lime: 'success',\n teal: 'success',\n amber: 'warning',\n yellow: 'warning',\n orange: 'warning',\n blue: 'info',\n cyan: 'info',\n sky: 'info',\n indigo: 'info',\n gray: 'neutral',\n grey: 'neutral',\n slate: 'neutral',\n zinc: 'neutral',\n stone: 'neutral',\n violet: 'brand',\n purple: 'brand',\n }\n if (named[trimmed]) return named[trimmed]\n const hexMatch = /^#?([0-9a-f]{6})$/i.exec(trimmed)\n if (!hexMatch) return undefined\n const hex = hexMatch[1]\n const r = parseInt(hex.slice(0, 2), 16)\n const g = parseInt(hex.slice(2, 4), 16)\n const b = parseInt(hex.slice(4, 6), 16)\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n const delta = max - min\n if (delta < 24) return 'neutral'\n let hue = 0\n if (max === r) hue = ((g - b) / delta) % 6\n else if (max === g) hue = (b - r) / delta + 2\n else hue = (r - g) / delta + 4\n hue = (hue * 60 + 360) % 360\n // Hue ranges (degrees): 0-20 red(error), 20-50 orange(warning), 50-80 yellow(warning),\n // 80-170 green(success), 170-260 blue/indigo(info), 260-320 violet/purple(brand),\n // 320-340 pink/magenta(pink), 340-360 red(error).\n if (hue < 20) return 'error'\n if (hue < 50) return 'warning'\n if (hue < 80) return 'warning'\n if (hue < 170) return 'success'\n if (hue < 260) return 'info'\n if (hue < 320) return 'brand'\n if (hue < 340) return 'pink'\n return 'error'\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { buildIlikeTerm } from '../db/buildIlikeTerm'\n\nexport type FilterOperator =\n | 'is' | 'is_not' | 'contains' | 'does_not_contain' | 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty'\n | 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'between'\n | 'is_before' | 'is_after'\n | 'is_any_of' | 'is_none_of'\n | 'is_true' | 'is_false'\n | 'has_any_of' | 'has_all_of' | 'has_none_of'\n\nexport type FilterFieldType = 'text' | 'number' | 'date' | 'select' | 'boolean' | 'tags'\n\nexport type FilterOptionTone = 'success' | 'error' | 'warning' | 'info' | 'neutral' | 'brand' | 'pink'\n\nexport type FilterOption = {\n value: string\n label: string\n tone?: FilterOptionTone\n}\n\nexport type FilterFieldDef = {\n key: string\n label: string\n type: FilterFieldType\n group?: string\n iconName?: string\n loadOptions?: (query?: string) => Promise<FilterOption[]>\n options?: FilterOption[]\n}\n\nexport type FilterJoinOperator = 'and' | 'or'\n\nexport type FilterCondition = {\n id: string\n field: string\n operator: FilterOperator\n value: unknown\n join?: FilterJoinOperator\n}\n\nexport type AdvancedFilterState = {\n logic: FilterJoinOperator\n conditions: FilterCondition[]\n}\n\nexport const OPERATORS_BY_FIELD_TYPE: Record<FilterFieldType, FilterOperator[]> = {\n text: ['is', 'is_not', 'contains', 'does_not_contain', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty'],\n number: ['equals', 'not_equals', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'between', 'is_empty'],\n date: ['is', 'is_before', 'is_after', 'between', 'is_empty', 'is_not_empty'],\n select: ['is', 'is_not', 'is_any_of', 'is_none_of', 'is_empty'],\n boolean: ['is_true', 'is_false'],\n tags: ['has_any_of', 'has_all_of', 'has_none_of', 'is_empty'],\n}\n\nexport function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {\n switch (fieldType) {\n case 'text': return 'contains'\n case 'number': return 'equals'\n case 'date': return 'is_after'\n case 'select': return 'is'\n case 'boolean': return 'is_true'\n case 'tags': return 'has_any_of'\n }\n}\n\nconst VALID_OPERATORS = new Set<string>([\n 'is', 'is_not', 'contains', 'does_not_contain', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty',\n 'equals', 'not_equals', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'between',\n 'is_before', 'is_after',\n 'is_any_of', 'is_none_of',\n 'is_true', 'is_false',\n 'has_any_of', 'has_all_of', 'has_none_of',\n])\n\nexport function isValidOperator(op: string): op is FilterOperator {\n return VALID_OPERATORS.has(op)\n}\n\nexport function isValuelessOperator(operator: FilterOperator): boolean {\n return operator === 'is_empty' || operator === 'is_not_empty' || operator === 'is_true' || operator === 'is_false'\n}\n\nexport function createEmptyCondition(): FilterCondition {\n return {\n id: crypto.randomUUID(),\n field: '',\n operator: 'contains',\n value: '',\n join: 'and',\n }\n}\n\nfunction normalizeJoinOperator(value: unknown, fallback: FilterJoinOperator = 'and'): FilterJoinOperator {\n return value === 'or' ? 'or' : value === 'and' ? 'and' : fallback\n}\n\nfunction parseSerializedFilterValue(value: unknown): unknown {\n if (typeof value !== 'string') return value ?? null\n const trimmed = value.trim()\n if (!trimmed) return value\n if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return value\n try {\n return JSON.parse(trimmed)\n } catch {\n return value\n }\n}\n\nexport function normalizeAdvancedFilterState(state: AdvancedFilterState): AdvancedFilterState {\n const logic = normalizeJoinOperator(state.logic)\n return {\n logic,\n conditions: state.conditions.map((condition, index) => ({\n ...condition,\n join: index === 0 ? 'and' : normalizeJoinOperator(condition.join, logic),\n })),\n }\n}\n\nexport function serializeAdvancedFilter(state: AdvancedFilterState): Record<string, string> {\n const params: Record<string, string> = {}\n const normalized = normalizeAdvancedFilterState(state)\n if (!normalized.conditions.length) return params\n params['filter[logic]'] = normalized.logic\n normalized.conditions.forEach((condition, index) => {\n const prefix = `filter[conditions][${index}]`\n params[`${prefix}[field]`] = condition.field\n params[`${prefix}[op]`] = condition.operator\n if (index > 0) {\n params[`${prefix}[join]`] = condition.join ?? normalized.logic\n }\n if (!isValuelessOperator(condition.operator) && condition.value != null) {\n params[`${prefix}[value]`] = typeof condition.value === 'object'\n ? JSON.stringify(condition.value)\n : String(condition.value)\n }\n })\n return params\n}\n\n/**\n * A condition field must name a column, never a Where combinator. `$and`/`$or`/`$not` share the\n * filter namespace with column names, so a client-supplied `filter[...][field]=$and` would compile\n * to a combinator key: at best it reaches the engine as a filter on a column that does not exist,\n * at worst it collides with a key the route itself emitted. Unlike the operator, the field name is\n * route-specific, so this is the one check that can be made centrally \u2014 dropped silently, the same\n * way an unrecognized operator is.\n */\nfunction isUsableFilterField(field: string): boolean {\n return field.length > 0 && !field.startsWith('$')\n}\n\nexport function deserializeAdvancedFilter(query: Record<string, unknown>): AdvancedFilterState | null {\n const logic = normalizeJoinOperator(query['filter[logic]'])\n const conditions: FilterCondition[] = []\n for (let i = 0; i < 20; i++) {\n const field = query[`filter[conditions][${i}][field]`]\n const op = query[`filter[conditions][${i}][op]`]\n if (typeof field !== 'string' || typeof op !== 'string') break\n if (!isValidOperator(op) || !isUsableFilterField(field)) continue\n const value = query[`filter[conditions][${i}][value]`]\n const join = i === 0\n ? 'and'\n : normalizeJoinOperator(query[`filter[conditions][${i}][join]`], logic)\n conditions.push({\n id: String(i),\n field,\n operator: op,\n value: parseSerializedFilterValue(value),\n join,\n })\n }\n\n if (!conditions.length) return null\n return normalizeAdvancedFilterState({ logic, conditions })\n}\n\nfunction buildConditionFilter(condition: FilterCondition): Record<string, unknown> | null {\n if (!condition.field || !condition.operator) return null\n const normalizeSingleValue = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n const normalizeListValue = (value: unknown): unknown[] => {\n const list = Array.isArray(value) ? value : [value]\n return list\n .map((entry) => normalizeSingleValue(entry))\n .filter((entry) => entry !== null)\n }\n const filter: Record<string, unknown> = {}\n switch (condition.operator) {\n case 'is':\n case 'equals':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $eq: normalizeSingleValue(condition.value) }\n break\n case 'is_not':\n case 'not_equals':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ne: normalizeSingleValue(condition.value) }\n break\n case 'contains':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value))) }\n break\n case 'does_not_contain':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $not: { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value))) } }\n break\n case 'starts_with':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value)), 'startsWith') }\n break\n case 'ends_with':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $ilike: buildIlikeTerm(String(normalizeSingleValue(condition.value)), 'endsWith') }\n break\n case 'is_empty':\n filter[condition.field] = { $exists: false }\n break\n case 'is_not_empty':\n filter[condition.field] = { $exists: true }\n break\n case 'greater_than':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gt: normalizeSingleValue(condition.value) }\n break\n case 'less_than':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lt: normalizeSingleValue(condition.value) }\n break\n case 'greater_or_equal':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gte: normalizeSingleValue(condition.value) }\n break\n case 'less_or_equal':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lte: normalizeSingleValue(condition.value) }\n break\n case 'between':\n if (Array.isArray(condition.value) && condition.value.length === 2) {\n const start = normalizeSingleValue(condition.value[0])\n const end = normalizeSingleValue(condition.value[1])\n if (start === null && end === null) return null\n if (start !== null && end !== null) {\n filter[condition.field] = { $gte: start, $lte: end }\n } else if (start !== null) {\n filter[condition.field] = { $gte: start }\n } else if (end !== null) {\n filter[condition.field] = { $lte: end }\n }\n }\n break\n case 'is_before':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $lt: normalizeSingleValue(condition.value) }\n break\n case 'is_after':\n if (normalizeSingleValue(condition.value) === null) return null\n filter[condition.field] = { $gt: normalizeSingleValue(condition.value) }\n break\n case 'is_any_of':\n case 'has_any_of':\n if (normalizeListValue(condition.value).length === 0) return null\n filter[condition.field] = { $in: normalizeListValue(condition.value) }\n break\n case 'is_none_of':\n case 'has_none_of':\n if (normalizeListValue(condition.value).length === 0) return null\n filter[condition.field] = { $nin: normalizeListValue(condition.value) }\n break\n case 'has_all_of': {\n const allOfValues = normalizeListValue(condition.value)\n if (allOfValues.length === 0) return null\n filter[condition.field] = { $contains: allOfValues }\n break\n }\n case 'is_true':\n filter[condition.field] = { $eq: true }\n break\n case 'is_false':\n filter[condition.field] = { $eq: false }\n break\n }\n return Object.keys(filter).length > 0 ? filter : null\n}\n\nexport function convertAdvancedFilterToWhere(state: AdvancedFilterState): Record<string, unknown> {\n const normalized = normalizeAdvancedFilterState(state)\n if (!normalized.conditions.length) return {}\n\n const conditionEntries = normalized.conditions\n .map((condition) => {\n const filter = buildConditionFilter(condition)\n return filter\n ? {\n join: normalizeJoinOperator(condition.join, normalized.logic),\n filter,\n }\n : null\n })\n .filter((entry): entry is { join: FilterJoinOperator; filter: Record<string, unknown> } => entry !== null)\n\n if (!conditionEntries.length) return {}\n\n let clauses: Record<string, unknown>[] = [conditionEntries[0].filter]\n for (const entry of conditionEntries.slice(1)) {\n if (entry.join === 'or') {\n clauses = [...clauses, entry.filter]\n continue\n }\n clauses = clauses.map((clause) => ({\n ...clause,\n ...entry.filter,\n }))\n }\n\n return clauses.length > 1 ? { $or: clauses } : clauses[0]\n}\n\n// -----------------------------------------------------------------------------\n// v2 tree serialization (advanced-filter-tree)\n// -----------------------------------------------------------------------------\n\nimport type {\n AdvancedFilterTree,\n FilterRule as TreeFilterRule,\n FilterGroup as TreeFilterGroup,\n FilterCombinator as TreeFilterCombinator,\n} from './advanced-filter-tree'\n\nexport function serializeTree(tree: AdvancedFilterTree): Record<string, string> {\n if (!treeHasRules(tree.root)) return {}\n const out: Record<string, string> = { 'filter[v]': '2' }\n serializeTreeGroup(tree.root, 'filter[root]', out)\n return out\n}\n\nfunction treeHasRules(group: TreeFilterGroup): boolean {\n return group.children.some((child) => child.type === 'rule' || treeHasRules(child))\n}\n\nfunction serializeTreeGroup(group: TreeFilterGroup, prefix: string, out: Record<string, string>): void {\n out[`${prefix}[combinator]`] = group.combinator\n group.children.forEach((child, idx) => {\n const childPrefix = `${prefix}[children][${idx}]`\n if (child.type === 'rule') serializeTreeRule(child, childPrefix, out)\n else { out[`${childPrefix}[type]`] = 'group'; serializeTreeGroup(child, childPrefix, out) }\n })\n}\n\nfunction serializeTreeRule(rule: TreeFilterRule, prefix: string, out: Record<string, string>): void {\n out[`${prefix}[type]`] = 'rule'\n out[`${prefix}[field]`] = rule.field\n out[`${prefix}[op]`] = rule.operator\n if (!isValuelessOperator(rule.operator) && rule.value != null) {\n out[`${prefix}[value]`] = typeof rule.value === 'object'\n ? JSON.stringify(rule.value)\n : String(rule.value)\n }\n}\n\nexport function deserializeTree(query: Record<string, unknown>): AdvancedFilterTree | null {\n if (query['filter[v]'] !== '2') return null\n const root = readTreeGroup('filter[root]', query)\n if (!root) return null\n return { root }\n}\n\nfunction readTreeGroup(prefix: string, query: Record<string, unknown>): TreeFilterGroup | null {\n const combRaw = query[`${prefix}[combinator]`]\n if (combRaw !== 'and' && combRaw !== 'or') return null\n const children: Array<TreeFilterRule | TreeFilterGroup> = []\n for (let i = 0; i < 64; i++) {\n const childPrefix = `${prefix}[children][${i}]`\n const type = query[`${childPrefix}[type]`]\n if (type === 'rule') {\n const field = query[`${childPrefix}[field]`]\n const op = query[`${childPrefix}[op]`]\n if (typeof field !== 'string' || typeof op !== 'string' || !isValidOperator(op)) continue\n if (!isUsableFilterField(field)) continue\n const rawVal = query[`${childPrefix}[value]`]\n children.push({\n id: crypto.randomUUID(),\n type: 'rule',\n field,\n operator: op as FilterOperator,\n value: parseSerializedFilterValue(rawVal),\n })\n } else if (type === 'group') {\n const sub = readTreeGroup(childPrefix, query)\n if (sub) children.push(sub)\n } else {\n break\n }\n }\n return {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: combRaw as TreeFilterCombinator,\n children,\n }\n}\n\n/**\n * Runtime discriminator: is this value an `AdvancedFilterState` (legacy flat\n * shape with `logic` + `conditions`) rather than an `AdvancedFilterTree`\n * (`{root: FilterGroup}`)?\n *\n * Used at the DataTable boundary to bridge legacy callers onto the new tree\n * model without forcing third-party module developers to migrate immediately.\n * See `BACKWARD_COMPATIBILITY.md` \u00A73 and the spec's \"Migration & Backward\n * Compatibility\" section.\n */\nexport function isAdvancedFilterState(value: unknown): value is AdvancedFilterState {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n return Array.isArray(record.conditions) && (record.logic === 'and' || record.logic === 'or')\n}\n\n/**\n * Convert legacy flat AdvancedFilterState into a tree under standard SQL precedence\n * (AND binds tighter than OR). Runs of consecutive AND-joined conditions become\n * AND-subgroups, and the OR connectors join those subgroups in a root OR-group.\n */\nexport function flatToTree(flat: AdvancedFilterState): AdvancedFilterTree {\n if (flat.conditions.length === 0) {\n return { root: { id: crypto.randomUUID(), type: 'group', combinator: 'and', children: [] } }\n }\n\n // Step A: split into AND-runs separated by OR connectors. The first row's `join`\n // is logically \"and\" (no left neighbor); we never use it as a separator.\n const andRuns: FilterCondition[][] = [[flat.conditions[0]]]\n for (let i = 1; i < flat.conditions.length; i++) {\n const c = flat.conditions[i]\n if (c.join === 'or') andRuns.push([c])\n else andRuns[andRuns.length - 1].push(c)\n }\n\n const ruleFromCondition = (c: FilterCondition): TreeFilterRule => ({\n id: crypto.randomUUID(),\n type: 'rule',\n field: c.field,\n operator: c.operator,\n value: c.value,\n })\n\n // Step B: each AND-run becomes either a rule (length 1) or an AND-group.\n const orChildren: Array<TreeFilterRule | TreeFilterGroup> = andRuns.map((run) => {\n if (run.length === 1) return ruleFromCondition(run[0])\n return {\n id: crypto.randomUUID(),\n type: 'group',\n combinator: 'and',\n children: run.map(ruleFromCondition),\n }\n })\n\n // Step C: zero or one OR-disjunct -> root combinator stays \"and\"; otherwise OR.\n if (orChildren.length === 1) {\n const only = orChildren[0]\n if (only.type === 'group') return { root: only }\n return { root: { id: crypto.randomUUID(), type: 'group', combinator: 'and', children: [only] } }\n }\n return {\n root: { id: crypto.randomUUID(), type: 'group', combinator: 'or', children: orChildren },\n }\n}\n\n/**\n * Map a dictionary entry's display color (named like 'red'/'green' or a hex like '#ef4444')\n * to a `FilterOptionTone`, so dictionary-backed select options can render with the\n * correct status dot in chips and value pills. Unknown / undefined inputs return\n * `undefined` so the consumer falls back to plain (no tone) rendering.\n */\nexport function mapDictionaryColorToTone(color: string | null | undefined): FilterOptionTone | undefined {\n if (!color || typeof color !== 'string') return undefined\n const trimmed = color.trim().toLowerCase()\n if (!trimmed) return undefined\n const named: Record<string, FilterOptionTone> = {\n red: 'error',\n crimson: 'error',\n pink: 'pink',\n rose: 'pink',\n fuchsia: 'pink',\n magenta: 'pink',\n green: 'success',\n emerald: 'success',\n lime: 'success',\n teal: 'success',\n amber: 'warning',\n yellow: 'warning',\n orange: 'warning',\n blue: 'info',\n cyan: 'info',\n sky: 'info',\n indigo: 'info',\n gray: 'neutral',\n grey: 'neutral',\n slate: 'neutral',\n zinc: 'neutral',\n stone: 'neutral',\n violet: 'brand',\n purple: 'brand',\n }\n if (named[trimmed]) return named[trimmed]\n const hexMatch = /^#?([0-9a-f]{6})$/i.exec(trimmed)\n if (!hexMatch) return undefined\n const hex = hexMatch[1]\n const r = parseInt(hex.slice(0, 2), 16)\n const g = parseInt(hex.slice(2, 4), 16)\n const b = parseInt(hex.slice(4, 6), 16)\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n const delta = max - min\n if (delta < 24) return 'neutral'\n let hue = 0\n if (max === r) hue = ((g - b) / delta) % 6\n else if (max === g) hue = (b - r) / delta + 2\n else hue = (r - g) / delta + 4\n hue = (hue * 60 + 360) % 360\n // Hue ranges (degrees): 0-20 red(error), 20-50 orange(warning), 50-80 yellow(warning),\n // 80-170 green(success), 170-260 blue/indigo(info), 260-320 violet/purple(brand),\n // 320-340 pink/magenta(pink), 340-360 red(error).\n if (hue < 20) return 'error'\n if (hue < 50) return 'warning'\n if (hue < 80) return 'warning'\n if (hue < 170) return 'success'\n if (hue < 260) return 'info'\n if (hue < 320) return 'brand'\n if (hue < 340) return 'pink'\n return 'error'\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.6985.1.fb93574faa';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6985.1.fb93574faa",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.8-develop.
|
|
108
|
+
"@open-mercato/cache": "0.6.8-develop.6985.1.fb93574faa",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { calculateBackoffDelayMs } from '../retry'
|
|
2
|
+
|
|
3
|
+
describe('calculateBackoffDelayMs', () => {
|
|
4
|
+
const originalRandom = Math.random
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
Math.random = originalRandom
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('grows exponentially by the default factor of 2 from a 1000ms base (jitter pinned to 0)', () => {
|
|
11
|
+
Math.random = () => 0
|
|
12
|
+
expect(calculateBackoffDelayMs(1)).toBe(1000)
|
|
13
|
+
expect(calculateBackoffDelayMs(2)).toBe(2000)
|
|
14
|
+
expect(calculateBackoffDelayMs(3)).toBe(4000)
|
|
15
|
+
expect(calculateBackoffDelayMs(4)).toBe(8000)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('adds jitter drawn from [0, maxJitterMs) on top of the exponential term', () => {
|
|
19
|
+
// Math.floor(0.5 * 1000) = 500 added to the 2000ms exponential term for attempt 2.
|
|
20
|
+
Math.random = () => 0.5
|
|
21
|
+
expect(calculateBackoffDelayMs(2)).toBe(2500)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('clamps non-positive attempt numbers so the exponent never goes negative', () => {
|
|
25
|
+
Math.random = () => 0
|
|
26
|
+
// attemptNumber 1, 0 and -5 all collapse to factor^0 = the base delay.
|
|
27
|
+
expect(calculateBackoffDelayMs(0)).toBe(1000)
|
|
28
|
+
expect(calculateBackoffDelayMs(-5)).toBe(1000)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('is fully deterministic when maxJitterMs is 0 (no Math.random call)', () => {
|
|
32
|
+
const randomSpy = jest.fn(() => 0.999)
|
|
33
|
+
Math.random = randomSpy
|
|
34
|
+
expect(calculateBackoffDelayMs(3, { maxJitterMs: 0 })).toBe(4000)
|
|
35
|
+
expect(randomSpy).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('honours custom base delay and factor', () => {
|
|
39
|
+
Math.random = () => 0
|
|
40
|
+
expect(calculateBackoffDelayMs(1, { baseDelayMs: 250, factor: 3 })).toBe(250)
|
|
41
|
+
expect(calculateBackoffDelayMs(2, { baseDelayMs: 250, factor: 3 })).toBe(750)
|
|
42
|
+
expect(calculateBackoffDelayMs(3, { baseDelayMs: 250, factor: 3 })).toBe(2250)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('keeps the jitter bound below maxJitterMs across the random range', () => {
|
|
46
|
+
for (const sample of [0, 0.25, 0.5, 0.9999999]) {
|
|
47
|
+
Math.random = () => sample
|
|
48
|
+
const delay = calculateBackoffDelayMs(1, { maxJitterMs: 1000 })
|
|
49
|
+
// Base (1000) + jitter in [0, 1000): never reaches base + maxJitterMs.
|
|
50
|
+
expect(delay).toBeGreaterThanOrEqual(1000)
|
|
51
|
+
expect(delay).toBeLessThan(2000)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type BackoffOptions = {
|
|
2
|
+
baseDelayMs?: number
|
|
3
|
+
maxJitterMs?: number
|
|
4
|
+
factor?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exponential backoff with jitter for delivery-retry scheduling.
|
|
9
|
+
*
|
|
10
|
+
* `delay = baseDelayMs * factor^(attemptNumber - 1) + random(0, maxJitterMs)`
|
|
11
|
+
*
|
|
12
|
+
* `attemptNumber` is 1-based (the number of the attempt that just failed). The jitter spreads
|
|
13
|
+
* simultaneous retries so a provider outage does not make every failed delivery re-fire at the
|
|
14
|
+
* same instant (thundering herd). Reusable across delivery pipelines that need identical backoff
|
|
15
|
+
* semantics instead of hand-rolling their own (currently the push delivery worker).
|
|
16
|
+
*/
|
|
17
|
+
export function calculateBackoffDelayMs(attemptNumber: number, options: BackoffOptions = {}): number {
|
|
18
|
+
const baseDelayMs = options.baseDelayMs ?? 1000
|
|
19
|
+
const maxJitterMs = options.maxJitterMs ?? 1000
|
|
20
|
+
const factor = options.factor ?? 2
|
|
21
|
+
const jitterMs = maxJitterMs > 0 ? Math.floor(Math.random() * maxJitterMs) : 0
|
|
22
|
+
return baseDelayMs * Math.pow(factor, Math.max(attemptNumber - 1, 0)) + jitterMs
|
|
23
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
convertAdvancedFilterToWhere,
|
|
5
5
|
deserializeAdvancedFilter,
|
|
6
|
+
deserializeTree,
|
|
6
7
|
serializeAdvancedFilter,
|
|
7
8
|
type AdvancedFilterState,
|
|
8
9
|
} from '../advanced-filter'
|
|
@@ -58,6 +59,41 @@ describe('advanced filter', () => {
|
|
|
58
59
|
})
|
|
59
60
|
})
|
|
60
61
|
|
|
62
|
+
it('drops conditions whose field names a Where combinator instead of a column', () => {
|
|
63
|
+
// `$and`/`$or`/`$not` live in the same key namespace as column names, so a condition naming one
|
|
64
|
+
// would compile to a combinator key and collide with whatever the route put there — including a
|
|
65
|
+
// scope predicate. Dropped like an unrecognized operator, leaving the remaining conditions.
|
|
66
|
+
expect(
|
|
67
|
+
deserializeAdvancedFilter({
|
|
68
|
+
'filter[conditions][0][field]': '$and',
|
|
69
|
+
'filter[conditions][0][op]': 'is_empty',
|
|
70
|
+
'filter[conditions][1][field]': 'platform',
|
|
71
|
+
'filter[conditions][1][op]': 'is',
|
|
72
|
+
'filter[conditions][1][value]': 'ios',
|
|
73
|
+
}),
|
|
74
|
+
).toEqual({
|
|
75
|
+
logic: 'and',
|
|
76
|
+
conditions: [{ id: '1', field: 'platform', operator: 'is', value: 'ios', join: 'and' }],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
deserializeAdvancedFilter({
|
|
81
|
+
'filter[conditions][0][field]': '$or',
|
|
82
|
+
'filter[conditions][0][op]': 'is_empty',
|
|
83
|
+
}),
|
|
84
|
+
).toBeNull()
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
deserializeTree({
|
|
88
|
+
'filter[v]': '2',
|
|
89
|
+
'filter[root][combinator]': 'and',
|
|
90
|
+
'filter[root][children][0][type]': 'rule',
|
|
91
|
+
'filter[root][children][0][field]': '$and',
|
|
92
|
+
'filter[root][children][0][op]': 'is_empty',
|
|
93
|
+
})?.root.children,
|
|
94
|
+
).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
61
97
|
it('ignores empty values for operators that require a concrete value', () => {
|
|
62
98
|
expect(
|
|
63
99
|
convertAdvancedFilterToWhere({
|
|
@@ -138,6 +138,18 @@ export function serializeAdvancedFilter(state: AdvancedFilterState): Record<stri
|
|
|
138
138
|
return params
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A condition field must name a column, never a Where combinator. `$and`/`$or`/`$not` share the
|
|
143
|
+
* filter namespace with column names, so a client-supplied `filter[...][field]=$and` would compile
|
|
144
|
+
* to a combinator key: at best it reaches the engine as a filter on a column that does not exist,
|
|
145
|
+
* at worst it collides with a key the route itself emitted. Unlike the operator, the field name is
|
|
146
|
+
* route-specific, so this is the one check that can be made centrally — dropped silently, the same
|
|
147
|
+
* way an unrecognized operator is.
|
|
148
|
+
*/
|
|
149
|
+
function isUsableFilterField(field: string): boolean {
|
|
150
|
+
return field.length > 0 && !field.startsWith('$')
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
export function deserializeAdvancedFilter(query: Record<string, unknown>): AdvancedFilterState | null {
|
|
142
154
|
const logic = normalizeJoinOperator(query['filter[logic]'])
|
|
143
155
|
const conditions: FilterCondition[] = []
|
|
@@ -145,7 +157,7 @@ export function deserializeAdvancedFilter(query: Record<string, unknown>): Advan
|
|
|
145
157
|
const field = query[`filter[conditions][${i}][field]`]
|
|
146
158
|
const op = query[`filter[conditions][${i}][op]`]
|
|
147
159
|
if (typeof field !== 'string' || typeof op !== 'string') break
|
|
148
|
-
if (!isValidOperator(op)) continue
|
|
160
|
+
if (!isValidOperator(op) || !isUsableFilterField(field)) continue
|
|
149
161
|
const value = query[`filter[conditions][${i}][value]`]
|
|
150
162
|
const join = i === 0
|
|
151
163
|
? 'and'
|
|
@@ -367,6 +379,7 @@ function readTreeGroup(prefix: string, query: Record<string, unknown>): TreeFilt
|
|
|
367
379
|
const field = query[`${childPrefix}[field]`]
|
|
368
380
|
const op = query[`${childPrefix}[op]`]
|
|
369
381
|
if (typeof field !== 'string' || typeof op !== 'string' || !isValidOperator(op)) continue
|
|
382
|
+
if (!isUsableFilterField(field)) continue
|
|
370
383
|
const rawVal = query[`${childPrefix}[value]`]
|
|
371
384
|
children.push({
|
|
372
385
|
id: crypto.randomUUID(),
|
|
@@ -67,6 +67,84 @@ export type NotificationTypeDefinition = {
|
|
|
67
67
|
linkHref?: string
|
|
68
68
|
Renderer?: ComponentType<NotificationRendererProps>
|
|
69
69
|
expiresAfterHours?: number
|
|
70
|
+
/**
|
|
71
|
+
* Optional i18n key for the short type name shown in a per-channel
|
|
72
|
+
* preferences UI (e.g. "Order created"). Distinct from `titleKey`, which is
|
|
73
|
+
* the per-instance message title. Falls back to `titleKey` when omitted.
|
|
74
|
+
*/
|
|
75
|
+
labelKey?: string
|
|
76
|
+
/** Optional i18n key for helper text shown beside the type in a preferences UI. */
|
|
77
|
+
descriptionKey?: string
|
|
78
|
+
/**
|
|
79
|
+
* Optional free-form grouping label (e.g. `security`, `orders`, `marketing`) so a
|
|
80
|
+
* client — typically a mobile app — can list/group notification types under a heading.
|
|
81
|
+
* Plain string, not an enum; mirrored to the `notification_types` table and returned by
|
|
82
|
+
* `GET /api/notifications/types`.
|
|
83
|
+
*
|
|
84
|
+
* Defaults to the prefix before the first dot in `type` (`sales.order.created` →
|
|
85
|
+
* `sales`), resolved once during `syncNotificationTypes`. Declare it only to override
|
|
86
|
+
* that default. Localize the heading by adding `notifications.categories.<key>` to YOUR
|
|
87
|
+
* OWN module's `i18n/*.json` — the `notifications` module deliberately ships no category
|
|
88
|
+
* labels and knows nothing about which categories exist. `GET /api/notifications/types`
|
|
89
|
+
* resolves the key into `categoryLabel`, falling back to the raw key when absent.
|
|
90
|
+
*
|
|
91
|
+
* Clients group on `category` (stable across locales) and display `categoryLabel`.
|
|
92
|
+
*/
|
|
93
|
+
category?: string
|
|
94
|
+
/**
|
|
95
|
+
* When `true`, this type's push is delivered as a silent / content-available (data-only)
|
|
96
|
+
* wake-up instead of a visible alert. `silent` selects the delivery STYLE only — the
|
|
97
|
+
* notification still flows through the normal `notificationService.create()` path (in-app
|
|
98
|
+
* row created, per-channel preferences respected unless the type is `nonOptOut`).
|
|
99
|
+
*/
|
|
100
|
+
silent?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* When `true`, the recipient cannot opt out of this type: delivery ignores any
|
|
103
|
+
* stored per-channel preference (security/account alerts that must always be
|
|
104
|
+
* delivered). A preferences UI should render it as locked/forced-on, and
|
|
105
|
+
* `setPreferences` refuses to store an opt-out row for it.
|
|
106
|
+
*/
|
|
107
|
+
nonOptOut?: boolean
|
|
108
|
+
/**
|
|
109
|
+
* When `true`, this type is hidden from the client-facing catalogue: it is NOT mirrored to the
|
|
110
|
+
* `notification_types` table and therefore not returned by `GET /api/notifications/types`, so a
|
|
111
|
+
* mobile app's preferences screen never lists it. Used for internal/admin-only types (e.g. one-off
|
|
112
|
+
* admin custom pushes) that are dispatched by the server, not toggled by users. The type still
|
|
113
|
+
* lives in the in-memory registry for delivery logic (`getNotificationType`).
|
|
114
|
+
*/
|
|
115
|
+
hiddenFromSettings?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* Optional per-type channel eligibility: the delivery channels this type may EVER use
|
|
118
|
+
* (e.g. a marketing type restricted to `['push']` so it never lands in the in-app bell).
|
|
119
|
+
* The dispatcher intersects it with the per-send target, the registered strategies, and the
|
|
120
|
+
* recipient's preferences via `shouldDeliver`. Omit to make the type eligible for every
|
|
121
|
+
* registered channel (pre-Phase-7 behavior).
|
|
122
|
+
*
|
|
123
|
+
* Operators can override this set per tenant via the `notification_type_overrides` table
|
|
124
|
+
* (`PATCH /api/notifications/types` / the Notification Delivery settings page): a stored
|
|
125
|
+
* array replaces the code-declared one, an absent row (or `NULL`) inherits it. A channel
|
|
126
|
+
* outside the effective set never delivers for the type in that tenant — the check runs
|
|
127
|
+
* before both the `nonOptOut` bypass and user preferences, and the preference UIs render
|
|
128
|
+
* that cell locked off.
|
|
129
|
+
*/
|
|
130
|
+
channels?: string[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A delivery channel in the module-registered channel catalogue. Any module can contribute channels
|
|
135
|
+
* by exporting `notificationChannels: NotificationChannelDefinition[]` from a `notification-channels.ts`
|
|
136
|
+
* file (generator-discovered), so a preferences UI and the `/api/notifications/channels` endpoint stay
|
|
137
|
+
* in sync with the actual delivery paths without a hardcoded list. `id` matches the delivery-strategy id
|
|
138
|
+
* (`in_app`, `email`, `push`, …) — treat it as a FROZEN contract once shipped.
|
|
139
|
+
*/
|
|
140
|
+
export type NotificationChannelDefinition = {
|
|
141
|
+
id: string
|
|
142
|
+
/** i18n key for the channel's display name in a preferences UI. */
|
|
143
|
+
labelKey: string
|
|
144
|
+
/** Optional i18n key for helper text shown beside the channel. */
|
|
145
|
+
descriptionKey?: string
|
|
146
|
+
/** Ascending display order; entries without an order sort after ordered ones, then by id. */
|
|
147
|
+
order?: number
|
|
70
148
|
}
|
|
71
149
|
|
|
72
150
|
export type NotificationDto = {
|
|
@@ -93,6 +171,10 @@ export type NotificationDto = {
|
|
|
93
171
|
sourceEntityType?: string | null
|
|
94
172
|
sourceEntityId?: string | null
|
|
95
173
|
linkHref?: string | null
|
|
174
|
+
/** Arbitrary app-readable key/values attached at create time (also delivered with the push). */
|
|
175
|
+
data?: Record<string, string> | null
|
|
176
|
+
/** Resolved delivery channels for this notification. `null` = all channels (legacy/untargeted). */
|
|
177
|
+
channels?: string[] | null
|
|
96
178
|
createdAt: string
|
|
97
179
|
readAt?: string | null
|
|
98
180
|
actionTaken?: string | null
|