piper-utils 1.1.67 → 1.1.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "piper-utils",
3
- "version": "1.1.67",
3
+ "version": "1.1.69",
4
4
  "description": "Utility library for Piper",
5
5
  "main": "bin/main.js",
6
6
  "scripts": {
@@ -239,6 +239,15 @@ export function bindAuditRequest(event, businessId, models) {
239
239
  const user = getCurrentUser(event);
240
240
  const auditEnabled = !!(getCompanySettings(event) || {}).auditEnabled;
241
241
  _.forEach(models, (model) => {
242
+ // Lazily ensure the audit table model + hooks are wired in this Lambda
243
+ // process. `attachAudit` is idempotent — first call registers, subsequent
244
+ // calls are no-ops. The returned sync() promise is intentionally not
245
+ // awaited; the table is expected to already exist from the migration
246
+ // Lambda, and the synchronous registration is what the request needs.
247
+ const attachPromise = attachAudit(model);
248
+ if (attachPromise && typeof attachPromise.catch === 'function') {
249
+ attachPromise.catch((err) => console.error('attachAudit sync failed for', model.name, err));
250
+ }
242
251
  modelOptions[model.name] = { user, businessId, auditEnabled };
243
252
  });
244
253
  }
@@ -280,6 +289,11 @@ export function bindAuditRequest(event, businessId, models) {
280
289
  export function bindAuditRequestForUser(user, businessId, models, opts = {}) {
281
290
  const auditEnabled = opts.auditEnabled !== false;
282
291
  _.forEach(models, (model) => {
292
+ // See bindAuditRequest for why this is here.
293
+ const attachPromise = attachAudit(model);
294
+ if (attachPromise && typeof attachPromise.catch === 'function') {
295
+ attachPromise.catch((err) => console.error('attachAudit sync failed for', model.name, err));
296
+ }
283
297
  modelOptions[model.name] = { user, businessId, auditEnabled };
284
298
  });
285
299
  }
@@ -29,9 +29,16 @@ const resetState = () => {
29
29
 
30
30
  describe('audit', () => {
31
31
  describe('bindAuditRequest', () => {
32
+ // Pre-populate auditModels so attachAudit's idempotency check skips the
33
+ // sequelize wiring; these tests focus on the bind/state behavior only.
34
+ const preAttachedModel = (name) => {
35
+ auditModels[name] = { create: () => Promise.resolve() };
36
+ return { name };
37
+ };
38
+
32
39
  it('binds user from JWT claims and businessId from arg', () => {
33
40
  resetState();
34
- const Model = { name: 'Customer' };
41
+ const Model = preAttachedModel('Customer');
35
42
  const event = buildEvent({ uid: '7', email: 'kevin@test.com', auditEnabled: true });
36
43
 
37
44
  bindAuditRequest(event, 'BIZ-1', [Model]);
@@ -44,7 +51,7 @@ describe('audit', () => {
44
51
 
45
52
  it('reads auditEnabled=false from custom:SET claim', () => {
46
53
  resetState();
47
- const Model = { name: 'Customer' };
54
+ const Model = preAttachedModel('Customer');
48
55
  const event = buildEvent({ auditEnabled: false });
49
56
 
50
57
  bindAuditRequest(event, 'BIZ-1', [Model]);
@@ -54,14 +61,36 @@ describe('audit', () => {
54
61
 
55
62
  it('binds multiple models in one call', () => {
56
63
  resetState();
57
- const A = { name: 'ModelA' };
58
- const B = { name: 'ModelB' };
64
+ const A = preAttachedModel('ModelA');
65
+ const B = preAttachedModel('ModelB');
59
66
 
60
67
  bindAuditRequest(buildEvent(), 'BIZ-1', [A, B]);
61
68
 
62
69
  expect(modelOptions['ModelA']).toBeDefined();
63
70
  expect(modelOptions['ModelB']).toBeDefined();
64
71
  });
72
+
73
+ it('lazily calls attachAudit when the model is not yet registered (cold-start regression)', () => {
74
+ resetState();
75
+ const definedAudit = { belongsTo: jasmine.createSpy('belongsTo'), sync: jasmine.createSpy('sync').and.resolveTo() };
76
+ const Model = {
77
+ name: 'Widget',
78
+ addHook: jasmine.createSpy('addHook'),
79
+ sequelize: {
80
+ define: jasmine.createSpy('define').and.returnValue(definedAudit),
81
+ Sequelize: { STRING: 'STRING_TYPE' }
82
+ }
83
+ };
84
+
85
+ bindAuditRequest(buildEvent(), 'BIZ-1', [Model]);
86
+
87
+ // attachAudit ran: hooks wired, model registered
88
+ expect(Model.sequelize.define).toHaveBeenCalled();
89
+ expect(Model.addHook).toHaveBeenCalledWith('afterCreate', jasmine.any(Function));
90
+ expect(auditModels['Widget']).toBeDefined();
91
+ // and the per-request state is set
92
+ expect(modelOptions['Widget']).toBeDefined();
93
+ });
65
94
  });
66
95
 
67
96
  describe('attachAudit', () => {
@@ -1,99 +1,99 @@
1
- import _ from 'lodash';
2
- import { ensurePartnerScope } from './accessContext.js';
3
- import { errorList } from '../../../requestResponse/errorCodes.js';
4
-
5
- /**
6
- * Scope a WHERE clause to the partner's own business (partnerBusinessId).
7
- * Use for "mine" resources: deal, activity, application, template.
8
- *
9
- * global → deletes where.businessId (super sees everything)
10
- * partner → sets where.businessId = partnerBusinessId
11
- * throws partnerNotConfigured if partnerBusinessId is missing
12
- * standard → no-op (createFilters already handled)
13
- *
14
- * @param {object} where - Sequelize WHERE clause to mutate.
15
- * @param {object} access - Access object from resolveAccess.
16
- * @param {{ getPartnerById: Function }} deps
17
- */
18
- export async function scopeToOwnBusiness(where, access, { getPartnerById } = {}) {
19
- if (!access) {
20
- throw errorList.unauthorized;
21
- }
22
- if (access.level === 'global') {
23
- delete where.businessId;
24
- return;
25
- }
26
- if (access.level === 'partner') {
27
- await ensurePartnerScope(access, { getPartnerById });
28
- if (!access.partnerBusinessId) {
29
- throw errorList.partnerNotConfigured;
30
- }
31
- where.businessId = access.partnerBusinessId;
32
- return;
33
- }
34
- // standard → no-op (createFilters already injected from JWT)
35
- }
36
-
37
- /**
38
- * Scope a WHERE clause to the partner's portfolio (businessIds array).
39
- * Use for "book" resources (future transaction lists, merchant inbox views).
40
- *
41
- * global → deletes where.businessId
42
- * partner → sets where.businessId = businessIds[]
43
- * standard → no-op
44
- *
45
- * @param {object} where - Sequelize WHERE clause to mutate.
46
- * @param {object} access - Access object from resolveAccess.
47
- * @param {{ getPartnerById: Function }} deps
48
- */
49
- export async function scopeToPartnerBook(where, access, { getPartnerById } = {}) {
50
- if (!access) {
51
- throw errorList.unauthorized;
52
- }
53
- if (access.level === 'global') {
54
- delete where.businessId;
55
- return;
56
- }
57
- if (access.level === 'partner') {
58
- await ensurePartnerScope(access, { getPartnerById });
59
- where.businessId = access.businessIds || [];
60
- return;
61
- }
62
- // standard → no-op
63
- }
64
-
65
- /**
66
- * Scope to the union of partnerBusinessId + businessIds.
67
- * Use for tickets — partner sees own tickets + portfolio merchants' tickets.
68
- *
69
- * global → deletes where.businessId
70
- * partner → sets where.businessId = [partnerBusinessId, ...businessIds]
71
- * standard → sets where.businessId = access.businessIds
72
- * (explicitly handled here because ticket routes may not call
73
- * createFilters before scoping, e.g. delete/resolve handlers)
74
- *
75
- * @param {object} where - Sequelize WHERE clause to mutate.
76
- * @param {object} access - Access object from resolveAccess.
77
- * @param {{ getPartnerById: Function }} deps
78
- */
79
- export async function scopeToBookUnionOwn(where, access, { getPartnerById } = {}) {
80
- if (!access) {
81
- throw errorList.unauthorized;
82
- }
83
- if (access.level === 'global') {
84
- delete where.businessId;
85
- return;
86
- }
87
- if (access.level === 'partner') {
88
- await ensurePartnerScope(access, { getPartnerById });
89
- const ids = _.uniq(
90
- [access.partnerBusinessId, ...(access.businessIds || [])].filter(Boolean)
91
- );
92
- where.businessId = ids;
93
- return;
94
- }
95
- if (access.level === 'standard') {
96
- where.businessId = access.businessIds;
97
- return;
98
- }
99
- }
1
+ import _ from 'lodash';
2
+ import { ensurePartnerScope } from './accessContext.js';
3
+ import { errorList } from '../../../requestResponse/errorCodes.js';
4
+
5
+ /**
6
+ * Scope a WHERE clause to the partner's own business (partnerBusinessId).
7
+ * Use for "mine" resources: deal, activity, application, template.
8
+ *
9
+ * global → deletes where.businessId (super sees everything)
10
+ * partner → sets where.businessId = partnerBusinessId
11
+ * throws partnerNotConfigured if partnerBusinessId is missing
12
+ * standard → no-op (createFilters already handled)
13
+ *
14
+ * @param {object} where - Sequelize WHERE clause to mutate.
15
+ * @param {object} access - Access object from resolveAccess.
16
+ * @param {{ getPartnerById: Function }} deps
17
+ */
18
+ export async function scopeToOwnBusiness(where, access, { getPartnerById } = {}) {
19
+ if (!access) {
20
+ throw errorList.unauthorized;
21
+ }
22
+ if (access.level === 'global') {
23
+ delete where.businessId;
24
+ return;
25
+ }
26
+ if (access.level === 'partner') {
27
+ await ensurePartnerScope(access, { getPartnerById });
28
+ if (!access.partnerBusinessId) {
29
+ throw errorList.partnerNotConfigured;
30
+ }
31
+ where.businessId = access.partnerBusinessId;
32
+ return;
33
+ }
34
+ // standard → no-op (createFilters already injected from JWT)
35
+ }
36
+
37
+ /**
38
+ * Scope a WHERE clause to the partner's portfolio (businessIds array).
39
+ * Use for "book" resources (future transaction lists, merchant inbox views).
40
+ *
41
+ * global → deletes where.businessId
42
+ * partner → sets where.businessId = businessIds[]
43
+ * standard → no-op
44
+ *
45
+ * @param {object} where - Sequelize WHERE clause to mutate.
46
+ * @param {object} access - Access object from resolveAccess.
47
+ * @param {{ getPartnerById: Function }} deps
48
+ */
49
+ export async function scopeToPartnerBook(where, access, { getPartnerById } = {}) {
50
+ if (!access) {
51
+ throw errorList.unauthorized;
52
+ }
53
+ if (access.level === 'global') {
54
+ delete where.businessId;
55
+ return;
56
+ }
57
+ if (access.level === 'partner') {
58
+ await ensurePartnerScope(access, { getPartnerById });
59
+ where.businessId = access.businessIds || [];
60
+ return;
61
+ }
62
+ // standard → no-op
63
+ }
64
+
65
+ /**
66
+ * Scope to the union of partnerBusinessId + businessIds.
67
+ * Use for tickets — partner sees own tickets + portfolio merchants' tickets.
68
+ *
69
+ * global → deletes where.businessId
70
+ * partner → sets where.businessId = [partnerBusinessId, ...businessIds]
71
+ * standard → sets where.businessId = access.businessIds
72
+ * (explicitly handled here because ticket routes may not call
73
+ * createFilters before scoping, e.g. delete/resolve handlers)
74
+ *
75
+ * @param {object} where - Sequelize WHERE clause to mutate.
76
+ * @param {object} access - Access object from resolveAccess.
77
+ * @param {{ getPartnerById: Function }} deps
78
+ */
79
+ export async function scopeToBookUnionOwn(where, access, { getPartnerById } = {}) {
80
+ if (!access) {
81
+ throw errorList.unauthorized;
82
+ }
83
+ if (access.level === 'global') {
84
+ delete where.businessId;
85
+ return;
86
+ }
87
+ if (access.level === 'partner') {
88
+ await ensurePartnerScope(access, { getPartnerById });
89
+ const ids = _.uniq(
90
+ [access.partnerBusinessId, ...(access.businessIds || [])].filter(Boolean)
91
+ );
92
+ where.businessId = ids;
93
+ return;
94
+ }
95
+ if (access.level === 'standard') {
96
+ where.businessId = access.businessIds;
97
+ return;
98
+ }
99
+ }
@@ -0,0 +1,287 @@
1
+ import {
2
+ scopeToOwnBusiness,
3
+ scopeToPartnerBook,
4
+ scopeToBookUnionOwn
5
+ } from './accessScope.js';
6
+ import { errorList } from '../../../requestResponse/errorCodes.js';
7
+
8
+ // Why these tests exist
9
+ // ---------------------
10
+ // The three scope helpers were rewritten to accept an explicit `requested`
11
+ // businessIds array from the caller's query string, and to intersect that
12
+ // against the role's allowed set instead of unconditionally overwriting
13
+ // where.businessId. The header "My Business" filter for super users depends
14
+ // on the new intersection — these tests pin the contract.
15
+ //
16
+ // "requested" is what the route handler extracts via getRequestedBusinessIds(event)
17
+ // and forwards. Empty array = no explicit filter (back-compat path).
18
+
19
+ describe('accessScope helpers', () => {
20
+
21
+ describe('scopeToBookUnionOwn — global (super)', () => {
22
+ it('deletes where.businessId when no explicit filter is requested', async () => {
23
+ // Arrange — super with no header pick → caller sends nothing
24
+ const where = { businessId: 'pre-existing-set-by-createFilters', active: true };
25
+ const access = { level: 'global' };
26
+
27
+ // Act
28
+ await scopeToBookUnionOwn(where, access, []);
29
+
30
+ // Assert — businessId stripped so the query is unconstrained;
31
+ // unrelated keys (active) survive
32
+ expect(where.businessId).toBeUndefined();
33
+ expect(where.active).toBe(true);
34
+ });
35
+
36
+ it('honors the requested businessIds when super passes them explicitly', async () => {
37
+ // Arrange — super picked business "X" in the header pill
38
+ const where = {};
39
+ const access = { level: 'global' };
40
+
41
+ // Act
42
+ await scopeToBookUnionOwn(where, access, ['X']);
43
+
44
+ // Assert — backend respects the explicit filter (the symptom from
45
+ // the user-reported "Deals list doesn't filter to My Business" bug)
46
+ expect(where.businessId).toEqual(['X']);
47
+ });
48
+ });
49
+
50
+ describe('scopeToBookUnionOwn — partner', () => {
51
+ it('returns the full union when no explicit filter is requested', async () => {
52
+ // Arrange — partner with own=O and portfolio=[A, B]; pre-resolved scope
53
+ const where = {};
54
+ const access = {
55
+ level: 'partner',
56
+ partnerBusinessId: 'O',
57
+ businessIds: ['A', 'B']
58
+ };
59
+
60
+ // Act
61
+ await scopeToBookUnionOwn(where, access, []);
62
+
63
+ // Assert — union preserved (own + portfolio, unique)
64
+ expect(where.businessId).toEqual(['O', 'A', 'B']);
65
+ });
66
+
67
+ it('intersects requested with the union when partner filters explicitly', async () => {
68
+ // Arrange — partner asks for A only, A is in portfolio
69
+ const where = {};
70
+ const access = {
71
+ level: 'partner',
72
+ partnerBusinessId: 'O',
73
+ businessIds: ['A', 'B']
74
+ };
75
+
76
+ // Act
77
+ await scopeToBookUnionOwn(where, access, ['A']);
78
+
79
+ // Assert — only the requested+allowed entry survives
80
+ expect(where.businessId).toEqual(['A']);
81
+ });
82
+
83
+ it('returns empty when requested business is outside the partner union', async () => {
84
+ // Arrange — partner asks for Z which is not in own or portfolio
85
+ const where = {};
86
+ const access = {
87
+ level: 'partner',
88
+ partnerBusinessId: 'O',
89
+ businessIds: ['A', 'B']
90
+ };
91
+
92
+ // Act
93
+ await scopeToBookUnionOwn(where, access, ['Z']);
94
+
95
+ // Assert — empty array → IN () → silent zero rows. Intentional;
96
+ // confirms a partner can't sneak past their scope via query param.
97
+ expect(where.businessId).toEqual([]);
98
+ });
99
+
100
+ it('lazy-loads partnerBusinessId/businessIds via getPartnerById on first call', async () => {
101
+ // Arrange — access is "fresh" (nulls), like resolveAccess hands out
102
+ const where = {};
103
+ const access = { level: 'partner', partnerId: 'p1', partnerBusinessId: null, businessIds: null };
104
+ const getPartnerById = jasmine.createSpy('getPartnerById').and.resolveTo({
105
+ partnerBusinessId: 'O',
106
+ businessIds: ['A']
107
+ });
108
+
109
+ // Act
110
+ await scopeToBookUnionOwn(where, access, [], { getPartnerById });
111
+
112
+ // Assert — DB hit happened and where.businessId is the resolved union
113
+ expect(getPartnerById).toHaveBeenCalledWith('p1');
114
+ expect(where.businessId).toEqual(['O', 'A']);
115
+ });
116
+ });
117
+
118
+ describe('scopeToBookUnionOwn — standard (merchant)', () => {
119
+ it('falls back to access.businessIds when no explicit filter is requested', async () => {
120
+ // Arrange — standard merchant has JWT businesses S1, S2
121
+ const where = {};
122
+ const access = { level: 'standard', businessIds: ['S1', 'S2'] };
123
+
124
+ // Act
125
+ await scopeToBookUnionOwn(where, access, []);
126
+
127
+ // Assert
128
+ expect(where.businessId).toEqual(['S1', 'S2']);
129
+ });
130
+
131
+ it('intersects requested with JWT businesses when standard filters explicitly', async () => {
132
+ // Arrange — merchant asks for S1 (in their JWT)
133
+ const where = {};
134
+ const access = { level: 'standard', businessIds: ['S1', 'S2'] };
135
+
136
+ // Act
137
+ await scopeToBookUnionOwn(where, access, ['S1']);
138
+
139
+ // Assert
140
+ expect(where.businessId).toEqual(['S1']);
141
+ });
142
+
143
+ it('returns empty when standard asks for a business outside their JWT', async () => {
144
+ // Arrange — IDOR attempt: merchant on S1 asks for someone else's business "X"
145
+ const where = {};
146
+ const access = { level: 'standard', businessIds: ['S1', 'S2'] };
147
+
148
+ // Act
149
+ await scopeToBookUnionOwn(where, access, ['X']);
150
+
151
+ // Assert — empty result, no cross-tenant leak
152
+ expect(where.businessId).toEqual([]);
153
+ });
154
+ });
155
+
156
+ describe('scopeToBookUnionOwn — defensive', () => {
157
+ it('throws unauthorized when access is null', async () => {
158
+ // Arrange — never expected to happen, but guard rather than corrupt the query
159
+ let thrown;
160
+
161
+ // Act
162
+ try {
163
+ await scopeToBookUnionOwn({}, null, []);
164
+ } catch (e) {
165
+ thrown = e;
166
+ }
167
+
168
+ // Assert
169
+ expect(thrown).toBe(errorList.unauthorized);
170
+ });
171
+ });
172
+
173
+ describe('scopeToOwnBusiness', () => {
174
+ it('sets where.businessId to partnerBusinessId for partners (no explicit filter)', async () => {
175
+ // Arrange
176
+ const where = {};
177
+ const access = { level: 'partner', partnerBusinessId: 'O', businessIds: ['A'] };
178
+
179
+ // Act
180
+ await scopeToOwnBusiness(where, access, []);
181
+
182
+ // Assert — own resource scope is single-value, not union
183
+ expect(where.businessId).toBe('O');
184
+ });
185
+
186
+ it('intersects requested with the partner own businessId', async () => {
187
+ // Arrange — partner asks for "O" (their own) — valid
188
+ const where = {};
189
+ const access = { level: 'partner', partnerBusinessId: 'O', businessIds: ['A'] };
190
+
191
+ // Act
192
+ await scopeToOwnBusiness(where, access, ['O']);
193
+
194
+ // Assert — intersection ['O'] ∩ ['O'] = ['O']
195
+ expect(where.businessId).toEqual(['O']);
196
+ });
197
+
198
+ it('returns empty when requested business is not the partner own', async () => {
199
+ // Arrange — partner asks for portfolio business "A" but this is the OWN scope
200
+ const where = {};
201
+ const access = { level: 'partner', partnerBusinessId: 'O', businessIds: ['A'] };
202
+
203
+ // Act
204
+ await scopeToOwnBusiness(where, access, ['A']);
205
+
206
+ // Assert — own scope rejects even valid-for-them businesses outside "own"
207
+ expect(where.businessId).toEqual([]);
208
+ });
209
+
210
+ it('throws partnerNotConfigured when partnerBusinessId is missing', async () => {
211
+ // Arrange — bad partner setup: portfolio set but no own businessId.
212
+ // Threw originally too, but now after the new requested param flows through.
213
+ const where = {};
214
+ const access = { level: 'partner', partnerBusinessId: null, businessIds: ['A'] };
215
+ let thrown;
216
+
217
+ // Act
218
+ try {
219
+ await scopeToOwnBusiness(where, access, []);
220
+ } catch (e) {
221
+ thrown = e;
222
+ }
223
+
224
+ // Assert
225
+ expect(thrown).toBe(errorList.partnerNotConfigured);
226
+ });
227
+
228
+ it('global with explicit request → where.businessId = requested', async () => {
229
+ // Arrange
230
+ const where = {};
231
+
232
+ // Act
233
+ await scopeToOwnBusiness(where, { level: 'global' }, ['X']);
234
+
235
+ // Assert
236
+ expect(where.businessId).toEqual(['X']);
237
+ });
238
+
239
+ it('global with no request → where.businessId deleted', async () => {
240
+ // Arrange
241
+ const where = { businessId: 'stale' };
242
+
243
+ // Act
244
+ await scopeToOwnBusiness(where, { level: 'global' }, []);
245
+
246
+ // Assert
247
+ expect(where.businessId).toBeUndefined();
248
+ });
249
+ });
250
+
251
+ describe('scopeToPartnerBook', () => {
252
+ it('returns full portfolio for partners with no explicit request', async () => {
253
+ // Arrange
254
+ const where = {};
255
+ const access = { level: 'partner', partnerBusinessId: 'O', businessIds: ['A', 'B'] };
256
+
257
+ // Act
258
+ await scopeToPartnerBook(where, access, []);
259
+
260
+ // Assert — book is portfolio only (no own)
261
+ expect(where.businessId).toEqual(['A', 'B']);
262
+ });
263
+
264
+ it('intersects requested with portfolio (excludes partner own)', async () => {
265
+ // Arrange — partner asks for "O" (own) and "A" (portfolio)
266
+ const where = {};
267
+ const access = { level: 'partner', partnerBusinessId: 'O', businessIds: ['A', 'B'] };
268
+
269
+ // Act
270
+ await scopeToPartnerBook(where, access, ['O', 'A']);
271
+
272
+ // Assert — book scope drops "own" even when explicitly requested
273
+ expect(where.businessId).toEqual(['A']);
274
+ });
275
+
276
+ it('global no request → deletes where.businessId', async () => {
277
+ // Arrange
278
+ const where = { businessId: 'stale' };
279
+
280
+ // Act
281
+ await scopeToPartnerBook(where, { level: 'global' }, []);
282
+
283
+ // Assert
284
+ expect(where.businessId).toBeUndefined();
285
+ });
286
+ });
287
+ });
@@ -1,38 +1,38 @@
1
- import { ensurePartnerScope } from './accessContext.js';
2
- import { scopeToOwnBusiness, scopeToPartnerBook, scopeToBookUnionOwn } from './accessScope.js';
3
- import {
4
- assertCanWriteOwnBusiness,
5
- assertCanWriteBookBusiness,
6
- assertCanWriteBookUnionOwn,
7
- stampOwnBusinessId
8
- } from './accessWrites.js';
9
-
10
- /**
11
- * Factory that pre-binds getPartnerById into all scope/assert/stamp helpers.
12
- * Bind once at module level, then call without repeating the injection:
13
- *
14
- * import { createAccessHelpers } from 'piper-utils';
15
- * import { getPartnerById } from '../partner/partner.js';
16
- *
17
- * const {
18
- * scopeToOwnBusiness,
19
- * stampOwnBusinessId,
20
- * assertCanWriteOwnBusiness
21
- * } = createAccessHelpers({ getPartnerById });
22
- *
23
- * @param {{ getPartnerById: Function }} deps
24
- * @returns {object} Pre-bound helpers.
25
- */
26
- export function createAccessHelpers({ getPartnerById }) {
27
- const deps = { getPartnerById };
28
- return {
29
- ensurePartnerScope: (access) => ensurePartnerScope(access, deps),
30
- scopeToOwnBusiness: (where, access) => scopeToOwnBusiness(where, access, deps),
31
- scopeToPartnerBook: (where, access) => scopeToPartnerBook(where, access, deps),
32
- scopeToBookUnionOwn: (where, access) => scopeToBookUnionOwn(where, access, deps),
33
- assertCanWriteOwnBusiness: (access, businessId) => assertCanWriteOwnBusiness(access, businessId, deps),
34
- assertCanWriteBookBusiness: (access, businessId) => assertCanWriteBookBusiness(access, businessId, deps),
35
- assertCanWriteBookUnionOwn: (access, businessId) => assertCanWriteBookUnionOwn(access, businessId, deps),
36
- stampOwnBusinessId: (access, body) => stampOwnBusinessId(access, body, deps)
37
- };
38
- }
1
+ import { ensurePartnerScope } from './accessContext.js';
2
+ import { scopeToOwnBusiness, scopeToPartnerBook, scopeToBookUnionOwn } from './accessScope.js';
3
+ import {
4
+ assertCanWriteOwnBusiness,
5
+ assertCanWriteBookBusiness,
6
+ assertCanWriteBookUnionOwn,
7
+ stampOwnBusinessId
8
+ } from './accessWrites.js';
9
+
10
+ /**
11
+ * Factory that pre-binds getPartnerById into all scope/assert/stamp helpers.
12
+ * Bind once at module level, then call without repeating the injection:
13
+ *
14
+ * import { createAccessHelpers } from 'piper-utils';
15
+ * import { getPartnerById } from '../partner/partner.js';
16
+ *
17
+ * const {
18
+ * scopeToOwnBusiness,
19
+ * stampOwnBusinessId,
20
+ * assertCanWriteOwnBusiness
21
+ * } = createAccessHelpers({ getPartnerById });
22
+ *
23
+ * @param {{ getPartnerById: Function }} deps
24
+ * @returns {object} Pre-bound helpers.
25
+ */
26
+ export function createAccessHelpers({ getPartnerById }) {
27
+ const deps = { getPartnerById };
28
+ return {
29
+ ensurePartnerScope: (access) => ensurePartnerScope(access, deps),
30
+ scopeToOwnBusiness: (where, access) => scopeToOwnBusiness(where, access, deps),
31
+ scopeToPartnerBook: (where, access) => scopeToPartnerBook(where, access, deps),
32
+ scopeToBookUnionOwn: (where, access) => scopeToBookUnionOwn(where, access, deps),
33
+ assertCanWriteOwnBusiness: (access, businessId) => assertCanWriteOwnBusiness(access, businessId, deps),
34
+ assertCanWriteBookBusiness: (access, businessId) => assertCanWriteBookBusiness(access, businessId, deps),
35
+ assertCanWriteBookUnionOwn: (access, businessId) => assertCanWriteBookUnionOwn(access, businessId, deps),
36
+ stampOwnBusinessId: (access, body) => stampOwnBusinessId(access, body, deps)
37
+ };
38
+ }