@porulle/core 0.21.0 → 0.23.0
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/auth/actor.d.ts.map +1 -1
- package/dist/auth/actor.js +42 -26
- package/dist/modules/cart/service.d.ts +7 -0
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +42 -2
- package/dist/test-utils/create-pglite-adapter.d.ts +15 -0
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +26 -2
- package/dist/test-utils/create-test-config.d.ts +2 -0
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +2 -2
- package/package.json +1 -1
- package/src/auth/actor.ts +62 -29
- package/src/modules/cart/service.ts +78 -2
- package/src/test-utils/create-pglite-adapter.ts +42 -2
- package/src/test-utils/create-test-config.ts +8 -3
package/dist/auth/actor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../src/auth/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../src/auth/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAmB/C,eAAO,MAAM,kBAAkB,OAAO,CAAC;AACvC,eAAO,MAAM,mBAAmB,qBAAwC,CAAC;AAEzE,eAAO,MAAM,4BAA4B,0JAS/B,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAEvE;AAkED,8EAA8E;AAC9E,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,OAAsD,GAC9D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAiDvB"}
|
package/dist/auth/actor.js
CHANGED
|
@@ -31,6 +31,44 @@ function toIsoString(value) {
|
|
|
31
31
|
const date = value instanceof Date ? value : typeof value === "string" ? new Date(value) : null;
|
|
32
32
|
return date !== null && Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The caller's role in one organization, by one indexed read of `member`.
|
|
36
|
+
*
|
|
37
|
+
* This used to go through the organization plugin's endpoints, and that cost six
|
|
38
|
+
* statements instead of one. `getFullOrganization` loads the organization row,
|
|
39
|
+
* its invitations and its ENTIRE member list, then scans in JavaScript for a
|
|
40
|
+
* single membership; `getActiveMemberRole` then looks for the same membership
|
|
41
|
+
* again. Both take `headers` and re-resolve the session from them, so each also
|
|
42
|
+
* re-reads `session` and `user`, and the plugin writes `active_organization_id`
|
|
43
|
+
* back to the session row — a write on the hot path of every GET.
|
|
44
|
+
*
|
|
45
|
+
* For a shopper every one of those is a guaranteed miss: a shopper is not a
|
|
46
|
+
* member of the platform organization and never will be. The member-by-
|
|
47
|
+
* organization scan also grows with the member list, so the platform's busiest
|
|
48
|
+
* request got slower as the platform got bigger.
|
|
49
|
+
*
|
|
50
|
+
* The adapter read below is the same query the plugin ended with, issued once
|
|
51
|
+
* and without re-resolving anything. `findOne` returning null IS the answer for
|
|
52
|
+
* a shopper — one miss, done.
|
|
53
|
+
*/
|
|
54
|
+
async function findMembershipRole(auth, userId, organizationId) {
|
|
55
|
+
try {
|
|
56
|
+
const context = await auth.$context;
|
|
57
|
+
const membership = await context?.adapter?.findOne({
|
|
58
|
+
model: "member",
|
|
59
|
+
where: [
|
|
60
|
+
{ field: "userId", value: userId },
|
|
61
|
+
{ field: "organizationId", value: organizationId },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
return membership?.role;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A membership that cannot be read is not a role. Treated as customer, as
|
|
68
|
+
// the plugin-endpoint version was, so this stays a performance change.
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
34
72
|
/** Resolve a better-auth session and its porulle organization permissions. */
|
|
35
73
|
export async function resolveActor(headers, auth, config, request = new Request("http://localhost", { headers })) {
|
|
36
74
|
let session;
|
|
@@ -52,32 +90,10 @@ export async function resolveActor(headers, auth, config, request = new Request(
|
|
|
52
90
|
const defaultOrgId = config.auth?.defaultOrganizationId ?? DEFAULT_ORG_ID;
|
|
53
91
|
let role = session.session.activeOrganizationRole;
|
|
54
92
|
let orgId = session.session.activeOrganizationId;
|
|
55
|
-
if (!role
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
headers,
|
|
60
|
-
});
|
|
61
|
-
if (org?.members) {
|
|
62
|
-
const membership = org.members.find((m) => m.userId === session.user.id);
|
|
63
|
-
if (membership) {
|
|
64
|
-
role = membership.role;
|
|
65
|
-
orgId = orgId ?? defaultOrgId;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// fall through — treat as customer
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!role && orgId && auth.api.getActiveMemberRole) {
|
|
74
|
-
try {
|
|
75
|
-
const roleResult = await auth.api.getActiveMemberRole({ headers });
|
|
76
|
-
role = roleResult?.role;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// fall through — treat as customer
|
|
80
|
-
}
|
|
93
|
+
if (!role) {
|
|
94
|
+
role = await findMembershipRole(auth, session.user.id, orgId ?? defaultOrgId);
|
|
95
|
+
if (role)
|
|
96
|
+
orgId = orgId ?? defaultOrgId;
|
|
81
97
|
}
|
|
82
98
|
if (!orgId && config.auth?.storeResolver) {
|
|
83
99
|
try {
|
|
@@ -37,6 +37,13 @@ export declare class CartService {
|
|
|
37
37
|
lineItems: CartLineItem[];
|
|
38
38
|
}>>;
|
|
39
39
|
addItem(input: AddCartItemInput, actor?: Actor | null, ctx?: TxContext, presentedSecret?: string): Promise<Result<CartLineItem>>;
|
|
40
|
+
/**
|
|
41
|
+
* The unit price for a new cart line, from the pricing step. Refuses — naming the entity and the
|
|
42
|
+
* currency — when no price is configured, when the pricing service is absent, or when resolution
|
|
43
|
+
* fails for any other reason: every one of those is a value the system cannot determine, and the
|
|
44
|
+
* defect this replaces was substituting a literal for exactly that.
|
|
45
|
+
*/
|
|
46
|
+
private resolveUnitPrice;
|
|
40
47
|
removeItem(cartId: string, itemId: string, actor?: Actor | null, ctx?: TxContext, presentedSecret?: string): Promise<Result<void>>;
|
|
41
48
|
updateQuantity(input: UpdateCartItemInput, actor?: Actor | null, ctx?: TxContext, presentedSecret?: string): Promise<Result<CartLineItem>>;
|
|
42
49
|
merge(sourceCartId: string, targetCartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAc5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAExE,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,cAAc,CAAC;IAC3B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AA+BD,qBAAa,WAAW;IAIV,OAAO,CAAC,IAAI;IAHxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;gBAE5B,IAAI,EAAE,eAAe;IAKnC,MAAM,CACV,KAAK,EAAE,eAAe,EACtB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IA0ElB,OAAO,CACX,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG;QAAE,SAAS,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC;IA6BxD;;;;OAIG;IACG,qBAAqB,CACzB,EAAE,EAAE,MAAM,EACV,cAAc,EAAE,MAAM,EACtB,GAAG,CAAC,EAAE,SAAS,GACd,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG;QAAE,SAAS,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC;IAWlD,OAAO,CACX,KAAK,EAAE,gBAAgB,EACvB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EACpB,GAAG,CAAC,EAAE,SAAS,EACf,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/cart/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -191,12 +191,35 @@ export class CartService {
|
|
|
191
191
|
item = updated;
|
|
192
192
|
}
|
|
193
193
|
else {
|
|
194
|
+
// A new line needs a price, and a price the system cannot determine is refused rather than
|
|
195
|
+
// invented. This used to read `processed.unitPriceSnapshot ?? 1000`: a silent default in a
|
|
196
|
+
// money path, which nothing in the response, the logs or the schema disclosed. An integrator
|
|
197
|
+
// found it by comparing a deployed cart reading 1000 against a catalog priced 14500-22800.
|
|
198
|
+
//
|
|
199
|
+
// A hook still wins when it supplies one, so bespoke pricing keeps its seam; otherwise the
|
|
200
|
+
// pricing step answers — the SAME step `resolveCurrentPrices` uses at checkout, so a cart and
|
|
201
|
+
// its order agree by construction rather than by an integrator remembering to install a hook.
|
|
202
|
+
const currency = processed.currency ?? cart.currency;
|
|
203
|
+
let unitPriceSnapshot = processed.unitPriceSnapshot;
|
|
204
|
+
if (unitPriceSnapshot === undefined) {
|
|
205
|
+
const resolved = await this.resolveUnitPrice({
|
|
206
|
+
entityId: processed.entityId,
|
|
207
|
+
currency,
|
|
208
|
+
quantity,
|
|
209
|
+
...(processed.variantId != null
|
|
210
|
+
? { variantId: processed.variantId }
|
|
211
|
+
: {}),
|
|
212
|
+
}, actor ?? null, ctx);
|
|
213
|
+
if (!resolved.ok)
|
|
214
|
+
return resolved;
|
|
215
|
+
unitPriceSnapshot = resolved.value;
|
|
216
|
+
}
|
|
194
217
|
item = await this.repo.createLineItem({
|
|
195
218
|
cartId: input.cartId,
|
|
196
219
|
entityId: processed.entityId,
|
|
197
220
|
quantity,
|
|
198
|
-
unitPriceSnapshot
|
|
199
|
-
currency
|
|
221
|
+
unitPriceSnapshot,
|
|
222
|
+
currency,
|
|
200
223
|
metadata: processed.metadata ?? {},
|
|
201
224
|
...(processed.variantId !== undefined
|
|
202
225
|
? { variantId: processed.variantId }
|
|
@@ -206,6 +229,23 @@ export class CartService {
|
|
|
206
229
|
await runAfterHooks(afterHooks, null, item, "addItem", context);
|
|
207
230
|
return Ok(item);
|
|
208
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The unit price for a new cart line, from the pricing step. Refuses — naming the entity and the
|
|
234
|
+
* currency — when no price is configured, when the pricing service is absent, or when resolution
|
|
235
|
+
* fails for any other reason: every one of those is a value the system cannot determine, and the
|
|
236
|
+
* defect this replaces was substituting a literal for exactly that.
|
|
237
|
+
*/
|
|
238
|
+
async resolveUnitPrice(input, actor, ctx) {
|
|
239
|
+
const pricing = this.deps.services.pricing;
|
|
240
|
+
if (typeof pricing?.resolve !== "function") {
|
|
241
|
+
return Err(new CommerceValidationError(`Cannot resolve a unit price for ${input.entityId}: no pricing service is configured.`));
|
|
242
|
+
}
|
|
243
|
+
const resolved = await pricing.resolve(input, actor, ctx);
|
|
244
|
+
if (!resolved.ok) {
|
|
245
|
+
return Err(new CommerceValidationError(`Cannot resolve a unit price for ${input.entityId} (${input.currency}). Configure a price for it, or supply unitPriceSnapshot from a cart.beforeAddItem hook.`));
|
|
246
|
+
}
|
|
247
|
+
return Ok(resolved.value.finalAmount);
|
|
248
|
+
}
|
|
209
249
|
async removeItem(cartId, itemId, actor, ctx, presentedSecret) {
|
|
210
250
|
try {
|
|
211
251
|
assertPermission(actor ?? null, "cart:update");
|
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { DatabaseAdapter } from "../kernel/database/adapter.js";
|
|
15
15
|
import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
|
|
16
|
+
/**
|
|
17
|
+
* Records the SQL statements issued between `start()` and `stop()`.
|
|
18
|
+
*
|
|
19
|
+
* Counting statements is the regression guard for round-trip cost: the queries
|
|
20
|
+
* behind an authenticated request take 0.3 ms of database time between them, so
|
|
21
|
+
* a timing assertion measures the network and tells you nothing you can act on,
|
|
22
|
+
* while a count is stable, fast and names exactly what regressed.
|
|
23
|
+
*/
|
|
24
|
+
export interface QueryLog {
|
|
25
|
+
/** Begin recording; clears anything previously recorded. */
|
|
26
|
+
start(): void;
|
|
27
|
+
/** Stop recording and return the statements captured, in order. */
|
|
28
|
+
stop(): string[];
|
|
29
|
+
}
|
|
16
30
|
/**
|
|
17
31
|
* Creates a PGlite-backed database adapter for testing.
|
|
18
32
|
*
|
|
@@ -28,5 +42,6 @@ export declare function createPGliteTestAdapter(): Promise<{
|
|
|
28
42
|
adapter: DatabaseAdapter;
|
|
29
43
|
db: DrizzleDatabase;
|
|
30
44
|
cleanup: () => Promise<void>;
|
|
45
|
+
queryLog: QueryLog;
|
|
31
46
|
}>;
|
|
32
47
|
//# sourceMappingURL=create-pglite-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-pglite-adapter.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-pglite-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAMrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"create-pglite-adapter.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-pglite-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAMrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,4DAA4D;IAC5D,KAAK,IAAI,IAAI,CAAC;IACd,mEAAmE;IACnE,IAAI,IAAI,MAAM,EAAE,CAAC;CAClB;AA0BD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,OAAO,EAAE,eAAe,CAAC;IACzB,EAAE,EAAE,eAAe,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC,CA6GD"}
|
|
@@ -50,8 +50,32 @@ async function pushCoreSchema(db) {
|
|
|
50
50
|
export async function createPGliteTestAdapter() {
|
|
51
51
|
// Create in-memory PGlite instance
|
|
52
52
|
const pg = new PGlite();
|
|
53
|
+
// Every statement Drizzle issues passes through this logger, and Better Auth's
|
|
54
|
+
// drizzleAdapter shares this same instance — so a recording covers the auth
|
|
55
|
+
// reads too. That is the point: the only honest regression guard for "this
|
|
56
|
+
// request costs N round trips" is a count of the statements, not a stopwatch.
|
|
57
|
+
const recorded = [];
|
|
58
|
+
let recording = false;
|
|
59
|
+
const queryLog = {
|
|
60
|
+
start() {
|
|
61
|
+
recorded.length = 0;
|
|
62
|
+
recording = true;
|
|
63
|
+
},
|
|
64
|
+
stop() {
|
|
65
|
+
recording = false;
|
|
66
|
+
return [...recorded];
|
|
67
|
+
},
|
|
68
|
+
};
|
|
53
69
|
// Wrap with Drizzle ORM first (pushSchema needs the Drizzle instance)
|
|
54
|
-
const db = drizzle(pg, {
|
|
70
|
+
const db = drizzle(pg, {
|
|
71
|
+
schema: fullSchema,
|
|
72
|
+
logger: {
|
|
73
|
+
logQuery(query) {
|
|
74
|
+
if (recording)
|
|
75
|
+
recorded.push(query);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
55
79
|
// Push core schema via drizzle-kit/api (no migration files needed)
|
|
56
80
|
// PgliteDatabase<Schema> and DrizzleDatabase share the same Schema type;
|
|
57
81
|
// the HKT parameter differs (PgliteQueryResultHKT vs PgQueryResultHKT)
|
|
@@ -125,5 +149,5 @@ export async function createPGliteTestAdapter() {
|
|
|
125
149
|
// Re-insert default org after truncation (CASCADE wipes it)
|
|
126
150
|
await ensureDefaultOrg(db);
|
|
127
151
|
}
|
|
128
|
-
return { adapter, db, cleanup };
|
|
152
|
+
return { adapter, db, cleanup, queryLog };
|
|
129
153
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommerceConfig } from "../config/types.js";
|
|
2
|
+
import type { QueryLog } from "./create-pglite-adapter.js";
|
|
2
3
|
export declare function createTestConfig(overrides?: Partial<CommerceConfig>): Promise<CommerceConfig>;
|
|
3
4
|
/**
|
|
4
5
|
* Creates a test config backed by PGlite (in-memory PostgreSQL).
|
|
@@ -14,5 +15,6 @@ export declare function createTestConfig(overrides?: Partial<CommerceConfig>): P
|
|
|
14
15
|
export declare function createPGliteTestConfig(overrides?: Partial<CommerceConfig>): Promise<{
|
|
15
16
|
config: CommerceConfig;
|
|
16
17
|
cleanup: () => Promise<void>;
|
|
18
|
+
queryLog: QueryLog;
|
|
17
19
|
}>;
|
|
18
20
|
//# sourceMappingURL=create-test-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-test-config.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-test-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"create-test-config.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-test-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AA8C3D,wBAAsB,gBAAgB,CACpC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC,cAAc,CAAC,CAqHzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM,GACtC,OAAO,CAAC;IACT,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC,CAUD"}
|
|
@@ -169,10 +169,10 @@ export async function createTestConfig(overrides = {}) {
|
|
|
169
169
|
*/
|
|
170
170
|
export async function createPGliteTestConfig(overrides = {}) {
|
|
171
171
|
const { createPGliteTestAdapter } = await import("./create-pglite-adapter.js");
|
|
172
|
-
const { adapter, cleanup } = await createPGliteTestAdapter();
|
|
172
|
+
const { adapter, cleanup, queryLog } = await createPGliteTestAdapter();
|
|
173
173
|
const config = await createTestConfig({
|
|
174
174
|
databaseAdapter: adapter,
|
|
175
175
|
...overrides,
|
|
176
176
|
});
|
|
177
|
-
return { config, cleanup };
|
|
177
|
+
return { config, cleanup, queryLog };
|
|
178
178
|
}
|
package/package.json
CHANGED
package/src/auth/actor.ts
CHANGED
|
@@ -4,6 +4,21 @@ import type { AuthInstance } from "./setup.js";
|
|
|
4
4
|
import { DEFAULT_ORG_ID } from "./org.js";
|
|
5
5
|
import { isCredentialRejection } from "./auth-failure.js";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The slice of Better Auth's internal context this file uses. Declared here
|
|
9
|
+
* rather than imported because `$context` is not on the public `Auth` type; if a
|
|
10
|
+
* future Better Auth removes or renames it, `findMembershipRole` degrades to
|
|
11
|
+
* "no role" rather than throwing, and the membership tests fail loudly.
|
|
12
|
+
*/
|
|
13
|
+
interface AuthContextLike {
|
|
14
|
+
adapter?: {
|
|
15
|
+
findOne<T>(query: {
|
|
16
|
+
model: string;
|
|
17
|
+
where: { field: string; value: unknown }[];
|
|
18
|
+
}): Promise<T | null>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
export const AUTH_COOKIE_PREFIX = "uc";
|
|
8
23
|
export const SESSION_COOKIE_NAME = `${AUTH_COOKIE_PREFIX}.session_token`;
|
|
9
24
|
|
|
@@ -42,6 +57,50 @@ function toIsoString(value: unknown): string | null {
|
|
|
42
57
|
return date !== null && Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
43
58
|
}
|
|
44
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The caller's role in one organization, by one indexed read of `member`.
|
|
62
|
+
*
|
|
63
|
+
* This used to go through the organization plugin's endpoints, and that cost six
|
|
64
|
+
* statements instead of one. `getFullOrganization` loads the organization row,
|
|
65
|
+
* its invitations and its ENTIRE member list, then scans in JavaScript for a
|
|
66
|
+
* single membership; `getActiveMemberRole` then looks for the same membership
|
|
67
|
+
* again. Both take `headers` and re-resolve the session from them, so each also
|
|
68
|
+
* re-reads `session` and `user`, and the plugin writes `active_organization_id`
|
|
69
|
+
* back to the session row — a write on the hot path of every GET.
|
|
70
|
+
*
|
|
71
|
+
* For a shopper every one of those is a guaranteed miss: a shopper is not a
|
|
72
|
+
* member of the platform organization and never will be. The member-by-
|
|
73
|
+
* organization scan also grows with the member list, so the platform's busiest
|
|
74
|
+
* request got slower as the platform got bigger.
|
|
75
|
+
*
|
|
76
|
+
* The adapter read below is the same query the plugin ended with, issued once
|
|
77
|
+
* and without re-resolving anything. `findOne` returning null IS the answer for
|
|
78
|
+
* a shopper — one miss, done.
|
|
79
|
+
*/
|
|
80
|
+
async function findMembershipRole(
|
|
81
|
+
auth: AuthInstance,
|
|
82
|
+
userId: string,
|
|
83
|
+
organizationId: string,
|
|
84
|
+
): Promise<string | undefined> {
|
|
85
|
+
try {
|
|
86
|
+
const context = await (
|
|
87
|
+
auth as unknown as { $context?: Promise<AuthContextLike> }
|
|
88
|
+
).$context;
|
|
89
|
+
const membership = await context?.adapter?.findOne<{ role?: string }>({
|
|
90
|
+
model: "member",
|
|
91
|
+
where: [
|
|
92
|
+
{ field: "userId", value: userId },
|
|
93
|
+
{ field: "organizationId", value: organizationId },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
return membership?.role;
|
|
97
|
+
} catch {
|
|
98
|
+
// A membership that cannot be read is not a role. Treated as customer, as
|
|
99
|
+
// the plugin-endpoint version was, so this stays a performance change.
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
45
104
|
/** Resolve a better-auth session and its porulle organization permissions. */
|
|
46
105
|
export async function resolveActor(
|
|
47
106
|
headers: Headers,
|
|
@@ -68,35 +127,9 @@ export async function resolveActor(
|
|
|
68
127
|
let role = session.session.activeOrganizationRole as string | undefined;
|
|
69
128
|
let orgId = session.session.activeOrganizationId as string | null;
|
|
70
129
|
|
|
71
|
-
if (!role
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
query: { organizationId: orgId ?? defaultOrgId },
|
|
75
|
-
headers,
|
|
76
|
-
});
|
|
77
|
-
if (org?.members) {
|
|
78
|
-
const membership = org.members.find(
|
|
79
|
-
(m) => m.userId === session.user.id,
|
|
80
|
-
);
|
|
81
|
-
if (membership) {
|
|
82
|
-
role = membership.role;
|
|
83
|
-
orgId = orgId ?? defaultOrgId;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// fall through — treat as customer
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!role && orgId && auth.api.getActiveMemberRole) {
|
|
92
|
-
try {
|
|
93
|
-
const roleResult = await auth.api.getActiveMemberRole({ headers });
|
|
94
|
-
role = (roleResult as Record<string, unknown>)?.role as
|
|
95
|
-
| string
|
|
96
|
-
| undefined;
|
|
97
|
-
} catch {
|
|
98
|
-
// fall through — treat as customer
|
|
99
|
-
}
|
|
130
|
+
if (!role) {
|
|
131
|
+
role = await findMembershipRole(auth, session.user.id, orgId ?? defaultOrgId);
|
|
132
|
+
if (role) orgId = orgId ?? defaultOrgId;
|
|
100
133
|
}
|
|
101
134
|
|
|
102
135
|
if (!orgId && config.auth?.storeResolver) {
|
|
@@ -318,13 +318,39 @@ export class CartService {
|
|
|
318
318
|
);
|
|
319
319
|
item = updated!;
|
|
320
320
|
} else {
|
|
321
|
+
// A new line needs a price, and a price the system cannot determine is refused rather than
|
|
322
|
+
// invented. This used to read `processed.unitPriceSnapshot ?? 1000`: a silent default in a
|
|
323
|
+
// money path, which nothing in the response, the logs or the schema disclosed. An integrator
|
|
324
|
+
// found it by comparing a deployed cart reading 1000 against a catalog priced 14500-22800.
|
|
325
|
+
//
|
|
326
|
+
// A hook still wins when it supplies one, so bespoke pricing keeps its seam; otherwise the
|
|
327
|
+
// pricing step answers — the SAME step `resolveCurrentPrices` uses at checkout, so a cart and
|
|
328
|
+
// its order agree by construction rather than by an integrator remembering to install a hook.
|
|
329
|
+
const currency = processed.currency ?? cart.currency;
|
|
330
|
+
let unitPriceSnapshot = processed.unitPriceSnapshot;
|
|
331
|
+
if (unitPriceSnapshot === undefined) {
|
|
332
|
+
const resolved = await this.resolveUnitPrice(
|
|
333
|
+
{
|
|
334
|
+
entityId: processed.entityId,
|
|
335
|
+
currency,
|
|
336
|
+
quantity,
|
|
337
|
+
...(processed.variantId != null
|
|
338
|
+
? { variantId: processed.variantId }
|
|
339
|
+
: {}),
|
|
340
|
+
},
|
|
341
|
+
actor ?? null,
|
|
342
|
+
ctx,
|
|
343
|
+
);
|
|
344
|
+
if (!resolved.ok) return resolved;
|
|
345
|
+
unitPriceSnapshot = resolved.value;
|
|
346
|
+
}
|
|
321
347
|
item = await this.repo.createLineItem(
|
|
322
348
|
{
|
|
323
349
|
cartId: input.cartId,
|
|
324
350
|
entityId: processed.entityId,
|
|
325
351
|
quantity,
|
|
326
|
-
unitPriceSnapshot
|
|
327
|
-
currency
|
|
352
|
+
unitPriceSnapshot,
|
|
353
|
+
currency,
|
|
328
354
|
metadata: processed.metadata ?? {},
|
|
329
355
|
...(processed.variantId !== undefined
|
|
330
356
|
? { variantId: processed.variantId }
|
|
@@ -339,6 +365,56 @@ export class CartService {
|
|
|
339
365
|
return Ok(item);
|
|
340
366
|
}
|
|
341
367
|
|
|
368
|
+
/**
|
|
369
|
+
* The unit price for a new cart line, from the pricing step. Refuses — naming the entity and the
|
|
370
|
+
* currency — when no price is configured, when the pricing service is absent, or when resolution
|
|
371
|
+
* fails for any other reason: every one of those is a value the system cannot determine, and the
|
|
372
|
+
* defect this replaces was substituting a literal for exactly that.
|
|
373
|
+
*/
|
|
374
|
+
private async resolveUnitPrice(
|
|
375
|
+
input: {
|
|
376
|
+
entityId: string;
|
|
377
|
+
currency: string;
|
|
378
|
+
quantity: number;
|
|
379
|
+
variantId?: string;
|
|
380
|
+
},
|
|
381
|
+
actor: Actor | null,
|
|
382
|
+
ctx?: TxContext,
|
|
383
|
+
): Promise<Result<number>> {
|
|
384
|
+
const pricing = this.deps.services.pricing as
|
|
385
|
+
| {
|
|
386
|
+
resolve(
|
|
387
|
+
params: {
|
|
388
|
+
entityId: string;
|
|
389
|
+
currency: string;
|
|
390
|
+
quantity: number;
|
|
391
|
+
variantId?: string;
|
|
392
|
+
},
|
|
393
|
+
actor?: Actor | null,
|
|
394
|
+
ctx?: TxContext,
|
|
395
|
+
): Promise<Result<{ finalAmount: number }>>;
|
|
396
|
+
}
|
|
397
|
+
| undefined;
|
|
398
|
+
|
|
399
|
+
if (typeof pricing?.resolve !== "function") {
|
|
400
|
+
return Err(
|
|
401
|
+
new CommerceValidationError(
|
|
402
|
+
`Cannot resolve a unit price for ${input.entityId}: no pricing service is configured.`,
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const resolved = await pricing.resolve(input, actor, ctx);
|
|
408
|
+
if (!resolved.ok) {
|
|
409
|
+
return Err(
|
|
410
|
+
new CommerceValidationError(
|
|
411
|
+
`Cannot resolve a unit price for ${input.entityId} (${input.currency}). Configure a price for it, or supply unitPriceSnapshot from a cart.beforeAddItem hook.`,
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return Ok(resolved.value.finalAmount);
|
|
416
|
+
}
|
|
417
|
+
|
|
342
418
|
async removeItem(
|
|
343
419
|
cartId: string,
|
|
344
420
|
itemId: string,
|
|
@@ -24,6 +24,21 @@ import { ensureDefaultOrg } from "../auth/org.js";
|
|
|
24
24
|
import * as fullSchema from "../kernel/database/schema.js";
|
|
25
25
|
import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Records the SQL statements issued between `start()` and `stop()`.
|
|
29
|
+
*
|
|
30
|
+
* Counting statements is the regression guard for round-trip cost: the queries
|
|
31
|
+
* behind an authenticated request take 0.3 ms of database time between them, so
|
|
32
|
+
* a timing assertion measures the network and tells you nothing you can act on,
|
|
33
|
+
* while a count is stable, fast and names exactly what regressed.
|
|
34
|
+
*/
|
|
35
|
+
export interface QueryLog {
|
|
36
|
+
/** Begin recording; clears anything previously recorded. */
|
|
37
|
+
start(): void;
|
|
38
|
+
/** Stop recording and return the statements captured, in order. */
|
|
39
|
+
stop(): string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
// drizzle-kit/api uses CJS internally; createRequire provides ESM compat.
|
|
28
43
|
const require = createRequire(import.meta.url);
|
|
29
44
|
|
|
@@ -63,12 +78,37 @@ export async function createPGliteTestAdapter(): Promise<{
|
|
|
63
78
|
adapter: DatabaseAdapter;
|
|
64
79
|
db: DrizzleDatabase;
|
|
65
80
|
cleanup: () => Promise<void>;
|
|
81
|
+
queryLog: QueryLog;
|
|
66
82
|
}> {
|
|
67
83
|
// Create in-memory PGlite instance
|
|
68
84
|
const pg = new PGlite();
|
|
69
85
|
|
|
86
|
+
// Every statement Drizzle issues passes through this logger, and Better Auth's
|
|
87
|
+
// drizzleAdapter shares this same instance — so a recording covers the auth
|
|
88
|
+
// reads too. That is the point: the only honest regression guard for "this
|
|
89
|
+
// request costs N round trips" is a count of the statements, not a stopwatch.
|
|
90
|
+
const recorded: string[] = [];
|
|
91
|
+
let recording = false;
|
|
92
|
+
const queryLog: QueryLog = {
|
|
93
|
+
start() {
|
|
94
|
+
recorded.length = 0;
|
|
95
|
+
recording = true;
|
|
96
|
+
},
|
|
97
|
+
stop() {
|
|
98
|
+
recording = false;
|
|
99
|
+
return [...recorded];
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
// Wrap with Drizzle ORM first (pushSchema needs the Drizzle instance)
|
|
71
|
-
const db = drizzle(pg, {
|
|
104
|
+
const db = drizzle(pg, {
|
|
105
|
+
schema: fullSchema,
|
|
106
|
+
logger: {
|
|
107
|
+
logQuery(query) {
|
|
108
|
+
if (recording) recorded.push(query);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
72
112
|
|
|
73
113
|
// Push core schema via drizzle-kit/api (no migration files needed)
|
|
74
114
|
// PgliteDatabase<Schema> and DrizzleDatabase share the same Schema type;
|
|
@@ -147,5 +187,5 @@ export async function createPGliteTestAdapter(): Promise<{
|
|
|
147
187
|
await ensureDefaultOrg(db);
|
|
148
188
|
}
|
|
149
189
|
|
|
150
|
-
return { adapter, db, cleanup };
|
|
190
|
+
return { adapter, db, cleanup, queryLog };
|
|
151
191
|
}
|
|
@@ -2,6 +2,7 @@ import { defineConfig } from "../config/define-config.js";
|
|
|
2
2
|
import type { CommerceConfig } from "../config/types.js";
|
|
3
3
|
import { Ok } from "../kernel/result.js";
|
|
4
4
|
import type { StorageAdapter } from "../modules/media/adapter.js";
|
|
5
|
+
import type { QueryLog } from "./create-pglite-adapter.js";
|
|
5
6
|
|
|
6
7
|
function createInMemoryStorageAdapter(): StorageAdapter {
|
|
7
8
|
const files = new Map<string, { data: ArrayBuffer; contentType: string }>();
|
|
@@ -181,14 +182,18 @@ export async function createTestConfig(
|
|
|
181
182
|
*/
|
|
182
183
|
export async function createPGliteTestConfig(
|
|
183
184
|
overrides: Partial<CommerceConfig> = {},
|
|
184
|
-
): Promise<{
|
|
185
|
+
): Promise<{
|
|
186
|
+
config: CommerceConfig;
|
|
187
|
+
cleanup: () => Promise<void>;
|
|
188
|
+
queryLog: QueryLog;
|
|
189
|
+
}> {
|
|
185
190
|
const { createPGliteTestAdapter } = await import("./create-pglite-adapter.js");
|
|
186
|
-
const { adapter, cleanup } = await createPGliteTestAdapter();
|
|
191
|
+
const { adapter, cleanup, queryLog } = await createPGliteTestAdapter();
|
|
187
192
|
|
|
188
193
|
const config = await createTestConfig({
|
|
189
194
|
databaseAdapter: adapter,
|
|
190
195
|
...overrides,
|
|
191
196
|
});
|
|
192
197
|
|
|
193
|
-
return { config, cleanup };
|
|
198
|
+
return { config, cleanup, queryLog };
|
|
194
199
|
}
|