@open-mercato/shared 0.4.11-develop.1534.b42f9ddcb3 → 0.4.11-develop.1537.8724d715df

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,4 +1,4 @@
1
- const APP_VERSION = "0.4.11-develop.1534.b42f9ddcb3";
1
+ const APP_VERSION = "0.4.11-develop.1537.8724d715df";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -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.4.11-develop.1534.b42f9ddcb3'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.11-develop.1537.8724d715df'\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.4.11-develop.1534.b42f9ddcb3",
3
+ "version": "0.4.11-develop.1537.8724d715df",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,56 @@
1
+ import {
2
+ FALSE_VALUES,
3
+ TRUE_VALUES,
4
+ parseBooleanFlag,
5
+ parseBooleanFromUnknown,
6
+ parseBooleanToken,
7
+ parseBooleanWithDefault,
8
+ } from '../boolean'
9
+
10
+ describe('boolean helpers', () => {
11
+ it('parses recognized truthy tokens regardless of case or surrounding whitespace', () => {
12
+ for (const token of TRUE_VALUES) {
13
+ expect(parseBooleanToken(token)).toBe(true)
14
+ expect(parseBooleanToken(` ${token.toUpperCase()} `)).toBe(true)
15
+ }
16
+ })
17
+
18
+ it('parses recognized falsy tokens regardless of case or surrounding whitespace', () => {
19
+ for (const token of FALSE_VALUES) {
20
+ expect(parseBooleanToken(token)).toBe(false)
21
+ expect(parseBooleanToken(` ${token.toUpperCase()} `)).toBe(false)
22
+ }
23
+ })
24
+
25
+ it('returns null for blank, invalid, or non-string token inputs', () => {
26
+ expect(parseBooleanToken('')).toBeNull()
27
+ expect(parseBooleanToken(' ')).toBeNull()
28
+ expect(parseBooleanToken('maybe')).toBeNull()
29
+ expect(parseBooleanToken(null)).toBeNull()
30
+ expect(parseBooleanToken(undefined)).toBeNull()
31
+ })
32
+
33
+ it('falls back only when the token cannot be parsed', () => {
34
+ expect(parseBooleanWithDefault('enabled', false)).toBe(true)
35
+ expect(parseBooleanWithDefault('disabled', true)).toBe(false)
36
+ expect(parseBooleanWithDefault('unknown', true)).toBe(true)
37
+ expect(parseBooleanWithDefault(undefined, false)).toBe(false)
38
+ })
39
+
40
+ it('returns undefined for unparseable flags while preserving parsed booleans', () => {
41
+ expect(parseBooleanFlag('yes')).toBe(true)
42
+ expect(parseBooleanFlag('no')).toBe(false)
43
+ expect(parseBooleanFlag('maybe')).toBeUndefined()
44
+ expect(parseBooleanFlag()).toBeUndefined()
45
+ })
46
+
47
+ it('parses booleans from unknown values without coercing unsupported types', () => {
48
+ expect(parseBooleanFromUnknown(true)).toBe(true)
49
+ expect(parseBooleanFromUnknown(false)).toBe(false)
50
+ expect(parseBooleanFromUnknown(' on ')).toBe(true)
51
+ expect(parseBooleanFromUnknown(' off ')).toBe(false)
52
+ expect(parseBooleanFromUnknown('unexpected')).toBeNull()
53
+ expect(parseBooleanFromUnknown(1)).toBeNull()
54
+ expect(parseBooleanFromUnknown({ value: 'true' })).toBeNull()
55
+ })
56
+ })
@@ -0,0 +1,216 @@
1
+ import type { EntityManager } from '@mikro-orm/core'
2
+ import { buildScopedWhere, extractScopeFromAuth, findOneScoped, softDelete } from '../crud'
3
+
4
+ class ExampleEntity {
5
+ id = ''
6
+ organizationId?: string | null
7
+ tenantId?: string | null
8
+ companyId?: string | null
9
+ workspaceId?: string | null
10
+ deletedAt?: Date | null
11
+ }
12
+
13
+ describe('buildScopedWhere', () => {
14
+ it('adds organization, tenant, and soft-delete filters without mutating the base object', () => {
15
+ const base = { id: 'entity-1' }
16
+
17
+ const where = buildScopedWhere(base, {
18
+ organizationId: 'org-1',
19
+ tenantId: 'tenant-1',
20
+ })
21
+
22
+ expect(where).toEqual({
23
+ id: 'entity-1',
24
+ organizationId: 'org-1',
25
+ tenantId: 'tenant-1',
26
+ deletedAt: null,
27
+ })
28
+ expect(where).not.toBe(base)
29
+ expect(base).toEqual({ id: 'entity-1' })
30
+ })
31
+
32
+ it('prefers organizationIds and collapses them to scalar or $in filters after sanitizing empty values', () => {
33
+ expect(
34
+ buildScopedWhere(
35
+ { id: 'entity-1' },
36
+ { organizationId: 'org-ignored', organizationIds: ['org-1'], tenantId: 'tenant-1' }
37
+ )
38
+ ).toEqual({
39
+ id: 'entity-1',
40
+ organizationId: 'org-1',
41
+ tenantId: 'tenant-1',
42
+ deletedAt: null,
43
+ })
44
+
45
+ expect(
46
+ buildScopedWhere(
47
+ { id: 'entity-1' },
48
+ { organizationIds: ['org-1', '', 'org-2'], tenantId: 'tenant-1' }
49
+ )
50
+ ).toEqual({
51
+ id: 'entity-1',
52
+ organizationId: { $in: ['org-1', 'org-2'] },
53
+ tenantId: 'tenant-1',
54
+ deletedAt: null,
55
+ })
56
+ })
57
+
58
+ it('fails closed when organizationIds is explicitly empty', () => {
59
+ expect(
60
+ buildScopedWhere(
61
+ { id: 'entity-1' },
62
+ { organizationIds: [], tenantId: 'tenant-1' }
63
+ )
64
+ ).toEqual({
65
+ id: 'entity-1',
66
+ organizationId: { $in: [] },
67
+ tenantId: 'tenant-1',
68
+ deletedAt: null,
69
+ })
70
+
71
+ expect(
72
+ buildScopedWhere(
73
+ { id: 'entity-1' },
74
+ { organizationIds: null, tenantId: 'tenant-1' }
75
+ )
76
+ ).toEqual({
77
+ id: 'entity-1',
78
+ organizationId: { $in: [] },
79
+ tenantId: 'tenant-1',
80
+ deletedAt: null,
81
+ })
82
+ })
83
+
84
+ it('supports custom scope fields and disabling implicit scope clauses', () => {
85
+ expect(
86
+ buildScopedWhere(
87
+ { id: 'entity-1' },
88
+ {
89
+ organizationId: 'org-1',
90
+ tenantId: 'tenant-1',
91
+ orgField: 'companyId',
92
+ tenantField: 'workspaceId',
93
+ softDeleteField: 'archivedAt',
94
+ }
95
+ )
96
+ ).toEqual({
97
+ id: 'entity-1',
98
+ companyId: 'org-1',
99
+ workspaceId: 'tenant-1',
100
+ archivedAt: null,
101
+ })
102
+
103
+ expect(
104
+ buildScopedWhere(
105
+ { id: 'entity-1' },
106
+ {
107
+ organizationId: 'org-1',
108
+ tenantId: 'tenant-1',
109
+ orgField: null,
110
+ tenantField: null,
111
+ softDeleteField: null,
112
+ }
113
+ )
114
+ ).toEqual({ id: 'entity-1' })
115
+ })
116
+
117
+ it('preserves an explicit null organization scope to keep queries fail-closed', () => {
118
+ expect(
119
+ buildScopedWhere(
120
+ { id: 'entity-1' },
121
+ { organizationId: null, tenantId: 'tenant-1' }
122
+ )
123
+ ).toEqual({
124
+ id: 'entity-1',
125
+ organizationId: null,
126
+ tenantId: 'tenant-1',
127
+ deletedAt: null,
128
+ })
129
+ })
130
+ })
131
+
132
+ describe('extractScopeFromAuth', () => {
133
+ it('returns an empty scope when auth is missing', () => {
134
+ expect(extractScopeFromAuth(null)).toEqual({})
135
+ expect(extractScopeFromAuth(undefined)).toEqual({})
136
+ })
137
+
138
+ it('maps auth fields and normalizes missing values to null', () => {
139
+ expect(extractScopeFromAuth({ orgId: 'org-1', tenantId: 'tenant-1' })).toEqual({
140
+ organizationId: 'org-1',
141
+ tenantId: 'tenant-1',
142
+ })
143
+
144
+ expect(extractScopeFromAuth({})).toEqual({
145
+ organizationId: null,
146
+ tenantId: null,
147
+ })
148
+ })
149
+ })
150
+
151
+ describe('findOneScoped', () => {
152
+ it('queries by id with the default organization and tenant fields when scope values are present', async () => {
153
+ const entity = new ExampleEntity()
154
+ entity.id = 'entity-1'
155
+ const findOne = jest.fn(async () => entity)
156
+ const getRepository = jest.fn(() => ({ findOne }))
157
+ const em = { getRepository } as unknown as EntityManager
158
+
159
+ const result = await findOneScoped(em, ExampleEntity, 'entity-1', {
160
+ organizationId: 'org-1',
161
+ tenantId: 'tenant-1',
162
+ })
163
+
164
+ expect(getRepository).toHaveBeenCalledWith(ExampleEntity)
165
+ expect(findOne).toHaveBeenCalledWith({
166
+ id: 'entity-1',
167
+ organizationId: 'org-1',
168
+ tenantId: 'tenant-1',
169
+ })
170
+ expect(result).toBe(entity)
171
+ })
172
+
173
+ it('omits nullable scope values and honors custom field names', async () => {
174
+ const findOne = jest.fn(async () => null)
175
+ const em = {
176
+ getRepository: jest.fn(() => ({ findOne })),
177
+ } as unknown as EntityManager
178
+
179
+ await findOneScoped(em, ExampleEntity, 'entity-2', {
180
+ organizationId: null,
181
+ tenantId: undefined,
182
+ orgField: 'companyId',
183
+ tenantField: 'workspaceId',
184
+ })
185
+
186
+ expect(findOne).toHaveBeenCalledWith({ id: 'entity-2' })
187
+
188
+ await findOneScoped(em, ExampleEntity, 'entity-3', {
189
+ organizationId: 'org-3',
190
+ tenantId: 'tenant-3',
191
+ orgField: 'companyId',
192
+ tenantField: 'workspaceId',
193
+ })
194
+
195
+ expect(findOne).toHaveBeenLastCalledWith({
196
+ id: 'entity-3',
197
+ companyId: 'org-3',
198
+ workspaceId: 'tenant-3',
199
+ })
200
+ })
201
+ })
202
+
203
+ describe('softDelete', () => {
204
+ it('sets deletedAt and persists the updated entity', async () => {
205
+ const entity = new ExampleEntity()
206
+ const persistAndFlush = jest.fn(async () => undefined)
207
+ const em = { persistAndFlush } as unknown as EntityManager
208
+ const before = Date.now()
209
+
210
+ await softDelete(em, entity)
211
+
212
+ expect(entity.deletedAt).toBeInstanceOf(Date)
213
+ expect((entity.deletedAt as Date).getTime()).toBeGreaterThanOrEqual(before)
214
+ expect(persistAndFlush).toHaveBeenCalledWith(entity)
215
+ })
216
+ })