@porulle/core 0.21.0 → 0.22.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/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/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 {
|
|
@@ -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) {
|
|
@@ -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
|
}
|