@open-mercato/shared 0.6.3-develop.3766.1.33102bfc91 → 0.6.3-develop.3778.1.25fdb35f2e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/crud/ids.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const MAX_IDS_PER_REQUEST = 200;
|
|
2
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-
|
|
2
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
3
|
function isUuid(value) {
|
|
4
4
|
return typeof value === "string" && UUID_REGEX.test(value);
|
|
5
5
|
}
|
|
@@ -17,14 +17,18 @@ function readExistingIds(filter) {
|
|
|
17
17
|
if (typeof filter === "string") {
|
|
18
18
|
return isUuid(filter) ? [filter] : null;
|
|
19
19
|
}
|
|
20
|
+
if (Array.isArray(filter)) {
|
|
21
|
+
return normalizeIdList(
|
|
22
|
+
filter.filter((value) => typeof value === "string")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
20
25
|
if (!filter || typeof filter !== "object") return null;
|
|
21
26
|
const operators = filter;
|
|
22
27
|
if (isUuid(operators.$eq)) return [operators.$eq];
|
|
23
28
|
if (Array.isArray(operators.$in)) {
|
|
24
|
-
|
|
29
|
+
return normalizeIdList(
|
|
25
30
|
operators.$in.filter((value) => typeof value === "string")
|
|
26
31
|
);
|
|
27
|
-
return parsed;
|
|
28
32
|
}
|
|
29
33
|
return null;
|
|
30
34
|
}
|
|
@@ -36,12 +40,15 @@ function parseIdsParam(raw, maxIds = MAX_IDS_PER_REQUEST) {
|
|
|
36
40
|
}
|
|
37
41
|
function mergeIdFilter(existingFilters, parsedIds) {
|
|
38
42
|
if (parsedIds.length === 0) return existingFilters;
|
|
39
|
-
const allowed = new Set(parsedIds);
|
|
40
43
|
const existingFilter = existingFilters.id;
|
|
41
44
|
const existingIds = readExistingIds(existingFilter);
|
|
42
45
|
if (!existingIds) {
|
|
43
|
-
|
|
46
|
+
if (existingFilter === void 0 || existingFilter === null) {
|
|
47
|
+
return { ...existingFilters, id: { $in: parsedIds } };
|
|
48
|
+
}
|
|
49
|
+
return existingFilters;
|
|
44
50
|
}
|
|
51
|
+
const allowed = new Set(parsedIds);
|
|
45
52
|
const intersection = existingIds.filter((id) => allowed.has(id));
|
|
46
53
|
return {
|
|
47
54
|
...existingFilters,
|
package/dist/lib/crud/ids.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/crud/ids.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Where } from '@open-mercato/shared/lib/query/types'\n\nexport const MAX_IDS_PER_REQUEST = 200\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-
|
|
5
|
-
"mappings": "AAEO,MAAM,sBAAsB;AAEnC,MAAM,aAAa;AAEnB,SAAS,OAAO,OAAiC;AAC/C,SAAO,OAAO,UAAU,YAAY,WAAW,KAAK,KAAK;AAC3D;AAEA,SAAS,gBAAgB,QAA4B;AACnD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,UAAU,oBAAI,IAAY;AAChC,aAAW,SAAS,QAAQ;AAC1B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,CAAC,OAAO,OAAO,EAAG;AAClC,YAAQ,IAAI,OAAO;AAAA,EACrB;AACA,SAAO,MAAM,KAAK,OAAO;AAC3B;AAEA,SAAS,gBAAgB,QAAkC;AACzD,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,OAAO,MAAM,IAAI,CAAC,MAAM,IAAI;AAAA,EACrC;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAElD,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,GAAG,EAAG,QAAO,CAAC,UAAU,GAAG;AAEhD,MAAI,MAAM,QAAQ,UAAU,GAAG,GAAG;AAChC,
|
|
4
|
+
"sourcesContent": ["import type { Where } from '@open-mercato/shared/lib/query/types'\n\nexport const MAX_IDS_PER_REQUEST = 200\n\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n\nfunction isUuid(value: unknown): value is string {\n return typeof value === 'string' && UUID_REGEX.test(value)\n}\n\nfunction normalizeIdList(values: string[]): string[] {\n if (values.length === 0) return values\n const deduped = new Set<string>()\n for (const value of values) {\n const trimmed = value.trim()\n if (!trimmed || !isUuid(trimmed)) continue\n deduped.add(trimmed)\n }\n return Array.from(deduped)\n}\n\nfunction readExistingIds(filter: unknown): string[] | null {\n if (typeof filter === 'string') {\n return isUuid(filter) ? [filter] : null\n }\n if (Array.isArray(filter)) {\n return normalizeIdList(\n filter.filter((value): value is string => typeof value === 'string'),\n )\n }\n if (!filter || typeof filter !== 'object') return null\n\n const operators = filter as Record<string, unknown>\n if (isUuid(operators.$eq)) return [operators.$eq]\n\n if (Array.isArray(operators.$in)) {\n return normalizeIdList(\n operators.$in.filter((value): value is string => typeof value === 'string'),\n )\n }\n\n return null\n}\n\nexport function parseIdsParam(raw: unknown, maxIds: number = MAX_IDS_PER_REQUEST): string[] {\n if (typeof raw !== 'string' || raw.trim().length === 0) return []\n const safeMax = Number.isFinite(maxIds) && maxIds > 0 ? Math.floor(maxIds) : MAX_IDS_PER_REQUEST\n const parsed = normalizeIdList(raw.split(','))\n return parsed.slice(0, safeMax)\n}\n\nexport function mergeIdFilter<Fields extends Record<string, unknown>>(\n existingFilters: Where<Fields>,\n parsedIds: string[],\n): Where<Fields> {\n if (parsedIds.length === 0) return existingFilters\n\n const existingFilter = (existingFilters as Record<string, unknown>).id\n const existingIds = readExistingIds(existingFilter)\n\n if (!existingIds) {\n // No existing narrowing \u2014 safe to install the user-supplied `$in`.\n if (existingFilter === undefined || existingFilter === null) {\n return { ...existingFilters, id: { $in: parsedIds } }\n }\n // Existing `id` filter is in a shape we do not recognise. Fail closed:\n // preserve the existing filter instead of widening to `parsedIds`. Adding\n // a new recognised shape to `readExistingIds` is preferable to silently\n // dropping a narrowing that another caller put in place.\n return existingFilters\n }\n\n const allowed = new Set(parsedIds)\n const intersection = existingIds.filter((id) => allowed.has(id))\n return {\n ...existingFilters,\n id: { $in: intersection },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,sBAAsB;AAEnC,MAAM,aAAa;AAEnB,SAAS,OAAO,OAAiC;AAC/C,SAAO,OAAO,UAAU,YAAY,WAAW,KAAK,KAAK;AAC3D;AAEA,SAAS,gBAAgB,QAA4B;AACnD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,UAAU,oBAAI,IAAY;AAChC,aAAW,SAAS,QAAQ;AAC1B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,CAAC,OAAO,OAAO,EAAG;AAClC,YAAQ,IAAI,OAAO;AAAA,EACrB;AACA,SAAO,MAAM,KAAK,OAAO;AAC3B;AAEA,SAAS,gBAAgB,QAAkC;AACzD,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,OAAO,MAAM,IAAI,CAAC,MAAM,IAAI;AAAA,EACrC;AACA,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO;AAAA,MACL,OAAO,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAAA,IACrE;AAAA,EACF;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAElD,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,GAAG,EAAG,QAAO,CAAC,UAAU,GAAG;AAEhD,MAAI,MAAM,QAAQ,UAAU,GAAG,GAAG;AAChC,WAAO;AAAA,MACL,UAAU,IAAI,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAAA,IAC5E;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,cAAc,KAAc,SAAiB,qBAA+B;AAC1F,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,EAAE,WAAW,EAAG,QAAO,CAAC;AAChE,QAAM,UAAU,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,KAAK,MAAM,MAAM,IAAI;AAC7E,QAAM,SAAS,gBAAgB,IAAI,MAAM,GAAG,CAAC;AAC7C,SAAO,OAAO,MAAM,GAAG,OAAO;AAChC;AAEO,SAAS,cACd,iBACA,WACe;AACf,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,QAAM,iBAAkB,gBAA4C;AACpE,QAAM,cAAc,gBAAgB,cAAc;AAElD,MAAI,CAAC,aAAa;AAEhB,QAAI,mBAAmB,UAAa,mBAAmB,MAAM;AAC3D,aAAO,EAAE,GAAG,iBAAiB,IAAI,EAAE,KAAK,UAAU,EAAE;AAAA,IACtD;AAKA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,IAAI,IAAI,SAAS;AACjC,QAAM,eAAe,YAAY,OAAO,CAAC,OAAO,QAAQ,IAAI,EAAE,CAAC;AAC/D,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI,EAAE,KAAK,aAAa;AAAA,EAC1B;AACF;",
|
|
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.3-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.3-develop.3778.1.25fdb35f2e'\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.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3778.1.25fdb35f2e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -89,13 +89,13 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@mikro-orm/core": "^7.
|
|
93
|
-
"@mikro-orm/decorators": "^7.
|
|
94
|
-
"@mikro-orm/postgresql": "^7.
|
|
95
|
-
"@open-mercato/cache": "0.6.3-develop.
|
|
92
|
+
"@mikro-orm/core": "^7.1.1",
|
|
93
|
+
"@mikro-orm/decorators": "^7.1.1",
|
|
94
|
+
"@mikro-orm/postgresql": "^7.1.1",
|
|
95
|
+
"@open-mercato/cache": "0.6.3-develop.3778.1.25fdb35f2e",
|
|
96
96
|
"dotenv": "^17.4.2",
|
|
97
97
|
"rate-limiter-flexible": "^11.1.0",
|
|
98
|
-
"re2js": "2.8.
|
|
98
|
+
"re2js": "2.8.3",
|
|
99
99
|
"reflect-metadata": "^0.2.2",
|
|
100
100
|
"sanitize-html": "^2.17.4",
|
|
101
101
|
"undici": "^8.3.0"
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"@types/jest": "^30.0.0",
|
|
105
105
|
"@types/sanitize-html": "^2.16.1",
|
|
106
106
|
"jest": "^30.4.2",
|
|
107
|
-
"ts-jest": "^29.4.
|
|
107
|
+
"ts-jest": "^29.4.11"
|
|
108
108
|
},
|
|
109
109
|
"publishConfig": {
|
|
110
110
|
"access": "public"
|
|
@@ -28,6 +28,13 @@ describe('crud ids helpers', () => {
|
|
|
28
28
|
expect(parseIdsParam(ids.join(','))).toHaveLength(MAX_IDS_PER_REQUEST)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
it('parseIdsParam accepts RFC 9562 UUID v6, v7, v8', () => {
|
|
32
|
+
const v6 = '1ec9414c-232a-6b00-b3c8-9e6bdeced846'
|
|
33
|
+
const v7 = '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'
|
|
34
|
+
const v8 = '320c3d4d-cc00-875b-8ec9-32363a3c8c4f'
|
|
35
|
+
expect(parseIdsParam(`${v6},${v7},${v8}`)).toEqual([v6, v7, v8])
|
|
36
|
+
})
|
|
37
|
+
|
|
31
38
|
it('mergeIdFilter adds id filter when none exists', () => {
|
|
32
39
|
expect(mergeIdFilter({}, [idA, idB])).toEqual({
|
|
33
40
|
id: { $in: [idA, idB] },
|
|
@@ -52,11 +59,54 @@ describe('crud ids helpers', () => {
|
|
|
52
59
|
})
|
|
53
60
|
})
|
|
54
61
|
|
|
55
|
-
it('mergeIdFilter
|
|
62
|
+
it('mergeIdFilter intersects existing plain-array narrowing', () => {
|
|
63
|
+
expect(mergeIdFilter({ id: [idA, idC] as any }, [idA, idB])).toEqual({
|
|
64
|
+
id: { $in: [idA] },
|
|
65
|
+
})
|
|
66
|
+
expect(mergeIdFilter({ id: [idC] as any }, [idA, idB])).toEqual({
|
|
67
|
+
id: { $in: [] },
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('mergeIdFilter intersects existing $eq with UUID v6/v7/v8', () => {
|
|
72
|
+
const v6 = '1ec9414c-232a-6b00-b3c8-9e6bdeced846'
|
|
73
|
+
const v7 = '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'
|
|
74
|
+
const v8 = '320c3d4d-cc00-875b-8ec9-32363a3c8c4f'
|
|
75
|
+
expect(mergeIdFilter({ id: { $eq: v6 } as any }, [v6, idA])).toEqual({
|
|
76
|
+
id: { $in: [v6] },
|
|
77
|
+
})
|
|
78
|
+
expect(mergeIdFilter({ id: { $eq: v7 } as any }, [idA])).toEqual({
|
|
79
|
+
id: { $in: [] },
|
|
80
|
+
})
|
|
81
|
+
expect(mergeIdFilter({ id: { $in: [v7, v8] } as any }, [v7, idB])).toEqual({
|
|
82
|
+
id: { $in: [v7] },
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('mergeIdFilter preserves existing filter for unknown filter shape (fail closed)', () => {
|
|
87
|
+
// Regression: previously the unrecognised shape was silently discarded and
|
|
88
|
+
// replaced with `{ id: { $in: parsedIds } }`, widening access. The contract
|
|
89
|
+
// is "intersect with existing id filters — never widen" — so unknown shapes
|
|
90
|
+
// must degrade closed.
|
|
56
91
|
expect(
|
|
57
|
-
mergeIdFilter({ id: { $ne: idA } }, [idA, idB]),
|
|
58
|
-
).toEqual({
|
|
59
|
-
|
|
92
|
+
mergeIdFilter({ id: { $ne: idA } as any }, [idA, idB]),
|
|
93
|
+
).toEqual({ id: { $ne: idA } })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('mergeIdFilter preserves existing $eq with unsupported value (fail closed)', () => {
|
|
97
|
+
// An `$eq` with a non-UUID payload is also an unrecognised narrowing — we
|
|
98
|
+
// must not drop it and substitute `parsedIds`.
|
|
99
|
+
expect(
|
|
100
|
+
mergeIdFilter({ id: { $eq: 'not-a-uuid' } as any }, [idA]),
|
|
101
|
+
).toEqual({ id: { $eq: 'not-a-uuid' } })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('mergeIdFilter treats undefined/null id as no existing narrowing', () => {
|
|
105
|
+
expect(mergeIdFilter({ id: undefined } as any, [idA])).toEqual({
|
|
106
|
+
id: { $in: [idA] },
|
|
107
|
+
})
|
|
108
|
+
expect(mergeIdFilter({ id: null } as any, [idA])).toEqual({
|
|
109
|
+
id: { $in: [idA] },
|
|
60
110
|
})
|
|
61
111
|
})
|
|
62
112
|
})
|
package/src/lib/crud/ids.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Where } from '@open-mercato/shared/lib/query/types'
|
|
|
2
2
|
|
|
3
3
|
export const MAX_IDS_PER_REQUEST = 200
|
|
4
4
|
|
|
5
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-
|
|
5
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
6
6
|
|
|
7
7
|
function isUuid(value: unknown): value is string {
|
|
8
8
|
return typeof value === 'string' && UUID_REGEX.test(value)
|
|
@@ -23,16 +23,20 @@ function readExistingIds(filter: unknown): string[] | null {
|
|
|
23
23
|
if (typeof filter === 'string') {
|
|
24
24
|
return isUuid(filter) ? [filter] : null
|
|
25
25
|
}
|
|
26
|
+
if (Array.isArray(filter)) {
|
|
27
|
+
return normalizeIdList(
|
|
28
|
+
filter.filter((value): value is string => typeof value === 'string'),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
26
31
|
if (!filter || typeof filter !== 'object') return null
|
|
27
32
|
|
|
28
33
|
const operators = filter as Record<string, unknown>
|
|
29
34
|
if (isUuid(operators.$eq)) return [operators.$eq]
|
|
30
35
|
|
|
31
36
|
if (Array.isArray(operators.$in)) {
|
|
32
|
-
|
|
37
|
+
return normalizeIdList(
|
|
33
38
|
operators.$in.filter((value): value is string => typeof value === 'string'),
|
|
34
39
|
)
|
|
35
|
-
return parsed
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
return null
|
|
@@ -51,14 +55,22 @@ export function mergeIdFilter<Fields extends Record<string, unknown>>(
|
|
|
51
55
|
): Where<Fields> {
|
|
52
56
|
if (parsedIds.length === 0) return existingFilters
|
|
53
57
|
|
|
54
|
-
const allowed = new Set(parsedIds)
|
|
55
58
|
const existingFilter = (existingFilters as Record<string, unknown>).id
|
|
56
59
|
const existingIds = readExistingIds(existingFilter)
|
|
57
60
|
|
|
58
61
|
if (!existingIds) {
|
|
59
|
-
|
|
62
|
+
// No existing narrowing — safe to install the user-supplied `$in`.
|
|
63
|
+
if (existingFilter === undefined || existingFilter === null) {
|
|
64
|
+
return { ...existingFilters, id: { $in: parsedIds } }
|
|
65
|
+
}
|
|
66
|
+
// Existing `id` filter is in a shape we do not recognise. Fail closed:
|
|
67
|
+
// preserve the existing filter instead of widening to `parsedIds`. Adding
|
|
68
|
+
// a new recognised shape to `readExistingIds` is preferable to silently
|
|
69
|
+
// dropping a narrowing that another caller put in place.
|
|
70
|
+
return existingFilters
|
|
60
71
|
}
|
|
61
72
|
|
|
73
|
+
const allowed = new Set(parsedIds)
|
|
62
74
|
const intersection = existingIds.filter((id) => allowed.has(id))
|
|
63
75
|
return {
|
|
64
76
|
...existingFilters,
|