@mosaic-code/prisma-deadlock-avoidance-tests 0.1.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/LICENSE +207 -0
- package/README.md +248 -0
- package/dist/assertions/row-assertion.d.ts +18 -0
- package/dist/assertions/row-assertion.d.ts.map +1 -0
- package/dist/assertions/row-assertion.js +53 -0
- package/dist/assertions/row-assertion.js.map +1 -0
- package/dist/assertions/table-assertion.d.ts +17 -0
- package/dist/assertions/table-assertion.d.ts.map +1 -0
- package/dist/assertions/table-assertion.js +33 -0
- package/dist/assertions/table-assertion.js.map +1 -0
- package/dist/extension.d.ts +60 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +322 -0
- package/dist/extension.js.map +1 -0
- package/dist/graphs/row-graph.d.ts +61 -0
- package/dist/graphs/row-graph.d.ts.map +1 -0
- package/dist/graphs/row-graph.js +231 -0
- package/dist/graphs/row-graph.js.map +1 -0
- package/dist/graphs/table-graph.d.ts +61 -0
- package/dist/graphs/table-graph.d.ts.map +1 -0
- package/dist/graphs/table-graph.js +123 -0
- package/dist/graphs/table-graph.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/caller-extractor.d.ts +27 -0
- package/dist/utils/caller-extractor.d.ts.map +1 -0
- package/dist/utils/caller-extractor.js +144 -0
- package/dist/utils/caller-extractor.js.map +1 -0
- package/dist/utils/primary-key-extractor.d.ts +23 -0
- package/dist/utils/primary-key-extractor.d.ts.map +1 -0
- package/dist/utils/primary-key-extractor.js +128 -0
- package/dist/utils/primary-key-extractor.js.map +1 -0
- package/dist/utils/raw-query-parser.d.ts +17 -0
- package/dist/utils/raw-query-parser.d.ts.map +1 -0
- package/dist/utils/raw-query-parser.js +58 -0
- package/dist/utils/raw-query-parser.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Graph } from '@dagrejs/graphlib';
|
|
2
|
+
import type { CallerInfo, TableEdgeLabel, TableCycleInfo } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages a directed graph of table lock ordering.
|
|
5
|
+
* Nodes represent tables, edges represent "table A was locked before table B".
|
|
6
|
+
*/
|
|
7
|
+
export declare class TableLockGraph {
|
|
8
|
+
private graph;
|
|
9
|
+
constructor();
|
|
10
|
+
/**
|
|
11
|
+
* Add an edge indicating that `from` table was locked before `to` table.
|
|
12
|
+
* Deduplicates callers by file:line.
|
|
13
|
+
*/
|
|
14
|
+
addEdge(from: string, to: string, caller: CallerInfo): void;
|
|
15
|
+
/**
|
|
16
|
+
* Merge edges from another graph into this one.
|
|
17
|
+
* Used to merge transaction graphs into the global graph.
|
|
18
|
+
*/
|
|
19
|
+
mergeFrom(other: TableLockGraph): void;
|
|
20
|
+
/**
|
|
21
|
+
* Check if the graph has any cycles
|
|
22
|
+
*/
|
|
23
|
+
hasCycles(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Find all cycles in the graph
|
|
26
|
+
*/
|
|
27
|
+
findCycles(): string[][];
|
|
28
|
+
/**
|
|
29
|
+
* Find cycles and build detailed cycle info including callers
|
|
30
|
+
*/
|
|
31
|
+
findCyclesWithCallers(): TableCycleInfo[];
|
|
32
|
+
/**
|
|
33
|
+
* Build detailed cycle info for a cycle
|
|
34
|
+
*/
|
|
35
|
+
private buildCycleInfo;
|
|
36
|
+
/**
|
|
37
|
+
* Get the edge label between two tables
|
|
38
|
+
*/
|
|
39
|
+
getEdge(from: string, to: string): TableEdgeLabel | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Get all tables in the graph
|
|
42
|
+
*/
|
|
43
|
+
getTables(): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Get all edges in the graph
|
|
46
|
+
*/
|
|
47
|
+
getEdges(): Array<{
|
|
48
|
+
from: string;
|
|
49
|
+
to: string;
|
|
50
|
+
label: TableEdgeLabel;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Clear all data from the graph
|
|
54
|
+
*/
|
|
55
|
+
reset(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Get the underlying graphlib Graph (for testing)
|
|
58
|
+
*/
|
|
59
|
+
getUnderlyingGraph(): Graph;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=table-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-graph.d.ts","sourceRoot":"","sources":["../../src/graphs/table-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAO,MAAM,mBAAmB,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAG7E;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAO;;IAMpB;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAsB3D;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAStC;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,UAAU,IAAI,MAAM,EAAE,EAAE;IAIxB;;OAEG;IACH,qBAAqB,IAAI,cAAc,EAAE;IAKzC;;OAEG;IACH,OAAO,CAAC,cAAc;IAwBtB;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI7D;;OAEG;IACH,SAAS,IAAI,MAAM,EAAE;IAIrB;;OAEG;IACH,QAAQ,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,CAAA;KAAE,CAAC;IAQtE;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,kBAAkB,IAAI,KAAK;CAG5B"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Graph, alg } from '@dagrejs/graphlib';
|
|
2
|
+
import { addCallerIfUnique } from '../utils/caller-extractor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages a directed graph of table lock ordering.
|
|
5
|
+
* Nodes represent tables, edges represent "table A was locked before table B".
|
|
6
|
+
*/
|
|
7
|
+
export class TableLockGraph {
|
|
8
|
+
graph;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.graph = new Graph({ directed: true });
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Add an edge indicating that `from` table was locked before `to` table.
|
|
14
|
+
* Deduplicates callers by file:line.
|
|
15
|
+
*/
|
|
16
|
+
addEdge(from, to, caller) {
|
|
17
|
+
// Ensure nodes exist
|
|
18
|
+
if (!this.graph.hasNode(from)) {
|
|
19
|
+
this.graph.setNode(from);
|
|
20
|
+
}
|
|
21
|
+
if (!this.graph.hasNode(to)) {
|
|
22
|
+
this.graph.setNode(to);
|
|
23
|
+
}
|
|
24
|
+
// Get or create edge label
|
|
25
|
+
const existingLabel = this.graph.edge(from, to);
|
|
26
|
+
if (existingLabel) {
|
|
27
|
+
// Deduplicate callers by file:line
|
|
28
|
+
addCallerIfUnique(existingLabel.callers, caller);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.graph.setEdge(from, to, { callers: [caller] });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Merge edges from another graph into this one.
|
|
36
|
+
* Used to merge transaction graphs into the global graph.
|
|
37
|
+
*/
|
|
38
|
+
mergeFrom(other) {
|
|
39
|
+
for (const edge of other.graph.edges()) {
|
|
40
|
+
const label = other.graph.edge(edge);
|
|
41
|
+
for (const caller of label.callers) {
|
|
42
|
+
this.addEdge(edge.v, edge.w, caller);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if the graph has any cycles
|
|
48
|
+
*/
|
|
49
|
+
hasCycles() {
|
|
50
|
+
return !alg.isAcyclic(this.graph);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Find all cycles in the graph
|
|
54
|
+
*/
|
|
55
|
+
findCycles() {
|
|
56
|
+
return alg.findCycles(this.graph);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find cycles and build detailed cycle info including callers
|
|
60
|
+
*/
|
|
61
|
+
findCyclesWithCallers() {
|
|
62
|
+
const cycles = this.findCycles();
|
|
63
|
+
return cycles.map((cycle) => this.buildCycleInfo(cycle));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build detailed cycle info for a cycle
|
|
67
|
+
*/
|
|
68
|
+
buildCycleInfo(cycle) {
|
|
69
|
+
const callers = [];
|
|
70
|
+
// For each consecutive pair of tables in the cycle, find the edge callers
|
|
71
|
+
for (let i = 0; i < cycle.length; i++) {
|
|
72
|
+
const from = cycle[i];
|
|
73
|
+
const to = cycle[(i + 1) % cycle.length];
|
|
74
|
+
const label = this.graph.edge(from, to);
|
|
75
|
+
if (label && label.callers.length > 0) {
|
|
76
|
+
// Add the first caller for this edge
|
|
77
|
+
callers.push({
|
|
78
|
+
table: to,
|
|
79
|
+
caller: label.callers[0],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
tables: cycle,
|
|
85
|
+
callers,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the edge label between two tables
|
|
90
|
+
*/
|
|
91
|
+
getEdge(from, to) {
|
|
92
|
+
return this.graph.edge(from, to);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get all tables in the graph
|
|
96
|
+
*/
|
|
97
|
+
getTables() {
|
|
98
|
+
return this.graph.nodes();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get all edges in the graph
|
|
102
|
+
*/
|
|
103
|
+
getEdges() {
|
|
104
|
+
return this.graph.edges().map((e) => ({
|
|
105
|
+
from: e.v,
|
|
106
|
+
to: e.w,
|
|
107
|
+
label: this.graph.edge(e),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear all data from the graph
|
|
112
|
+
*/
|
|
113
|
+
reset() {
|
|
114
|
+
this.graph = new Graph({ directed: true });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the underlying graphlib Graph (for testing)
|
|
118
|
+
*/
|
|
119
|
+
getUnderlyingGraph() {
|
|
120
|
+
return this.graph;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=table-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-graph.js","sourceRoot":"","sources":["../../src/graphs/table-graph.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { withDeadlockDetection } from './extension.js';
|
|
2
|
+
export { assertConsistentTableLocking, assertConsistentRowLocking, assertNoDeadlockRisk, resetDeadlockDetection, trackForUpdate, } from './extension.js';
|
|
3
|
+
export { TableLockingAssertionError } from './assertions/table-assertion.js';
|
|
4
|
+
export { RowLockingAssertionError } from './assertions/row-assertion.js';
|
|
5
|
+
export type { CallerInfo, StrictMode, RowLockingOptions, TableCycleInfo, RowCycleInfo, DeadlockDetectionConfig, } from './types.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAGtD,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,GACf,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAGxE,YAAY,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,uBAAuB,GACxB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Main extension
|
|
2
|
+
export { withDeadlockDetection } from './extension.js';
|
|
3
|
+
// Assertion functions
|
|
4
|
+
export { assertConsistentTableLocking, assertConsistentRowLocking, assertNoDeadlockRisk, resetDeadlockDetection, trackForUpdate, } from './extension.js';
|
|
5
|
+
// Error types
|
|
6
|
+
export { TableLockingAssertionError } from './assertions/table-assertion.js';
|
|
7
|
+
export { RowLockingAssertionError } from './assertions/row-assertion.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAEtD,sBAAsB;AACtB,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,GACf,MAAM,gBAAgB,CAAA;AAEvB,cAAc;AACd,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Information about where a locking operation was called from
|
|
3
|
+
*/
|
|
4
|
+
export interface CallerInfo {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
functionName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Operations that acquire locks on database records
|
|
12
|
+
*/
|
|
13
|
+
export type LockingOperation = 'findUniqueForUpdate' | 'findFirstForUpdate' | 'findManyForUpdate' | 'update' | 'updateMany' | 'delete' | 'deleteMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' | '$queryRaw' | '$queryRawUnsafe' | '$executeRaw' | '$executeRawUnsafe';
|
|
14
|
+
/**
|
|
15
|
+
* Strict ordering modes for row locking assertions
|
|
16
|
+
* - 'ASC': Primary keys must always be locked in ascending order
|
|
17
|
+
* - 'DESC': Primary keys must always be locked in descending order
|
|
18
|
+
* - true: Primary keys must be consistently ordered (either all ASC or all DESC)
|
|
19
|
+
* - false: No ordering requirement, only cycles are violations
|
|
20
|
+
*/
|
|
21
|
+
export type StrictMode = 'ASC' | 'DESC' | true | false;
|
|
22
|
+
/**
|
|
23
|
+
* Options for assertConsistentRowLocking
|
|
24
|
+
*/
|
|
25
|
+
export interface RowLockingOptions {
|
|
26
|
+
/** Specific tables to check. If empty/undefined, checks all tables. */
|
|
27
|
+
tables?: string[];
|
|
28
|
+
/** Ordering strictness mode. Defaults to false. */
|
|
29
|
+
strict?: StrictMode;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Information about a cycle detected in table locking order
|
|
33
|
+
*/
|
|
34
|
+
export interface TableCycleInfo {
|
|
35
|
+
/** Tables involved in the cycle, in order */
|
|
36
|
+
tables: string[];
|
|
37
|
+
/** Callers that contributed to the cycle */
|
|
38
|
+
callers: Array<{
|
|
39
|
+
table: string;
|
|
40
|
+
caller: CallerInfo;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Information about a cycle or ordering violation in row locking
|
|
45
|
+
*/
|
|
46
|
+
export interface RowCycleInfo {
|
|
47
|
+
/** The table where the violation occurred */
|
|
48
|
+
table: string;
|
|
49
|
+
/** Primary keys involved in the cycle/violation */
|
|
50
|
+
primaryKeys: Array<string | number>;
|
|
51
|
+
/** Callers that contributed to the issue */
|
|
52
|
+
callers: CallerInfo[];
|
|
53
|
+
/** Optional message describing the violation */
|
|
54
|
+
message?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Label stored on table graph edges
|
|
58
|
+
*/
|
|
59
|
+
export interface TableEdgeLabel {
|
|
60
|
+
/** All callers that created this edge (deduplicated by file:line) */
|
|
61
|
+
callers: CallerInfo[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Label stored on row graph edges
|
|
65
|
+
*/
|
|
66
|
+
export interface RowEdgeLabel {
|
|
67
|
+
/** All callers that created this edge */
|
|
68
|
+
callers: CallerInfo[];
|
|
69
|
+
/** The table this edge belongs to */
|
|
70
|
+
table: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Configuration options for the deadlock detection extension
|
|
74
|
+
*/
|
|
75
|
+
export interface DeadlockDetectionConfig {
|
|
76
|
+
/** Whether detection is enabled. Defaults to true. */
|
|
77
|
+
enabled?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Model metadata from Prisma DMMF
|
|
81
|
+
*/
|
|
82
|
+
export interface ModelMeta {
|
|
83
|
+
name: string;
|
|
84
|
+
dbName: string | null;
|
|
85
|
+
fields: ModelField[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Field metadata from Prisma DMMF
|
|
89
|
+
*/
|
|
90
|
+
export interface ModelField {
|
|
91
|
+
name: string;
|
|
92
|
+
kind: 'scalar' | 'object' | 'enum' | 'unsupported';
|
|
93
|
+
type: string;
|
|
94
|
+
dbName: string | null;
|
|
95
|
+
isId: boolean;
|
|
96
|
+
isUnique: boolean;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,qBAAqB,GACrB,oBAAoB,GACpB,mBAAmB,GACnB,QAAQ,GACR,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,qBAAqB,GACrB,QAAQ,GACR,WAAW,GACX,iBAAiB,GACjB,aAAa,GACb,mBAAmB,CAAA;AAEvB;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAA;AAEtD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4CAA4C;IAC5C,OAAO,EAAE,KAAK,CAAC;QACb,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,UAAU,CAAA;KACnB,CAAC,CAAA;CACH;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,WAAW,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;IACnC,4CAA4C;IAC5C,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,OAAO,EAAE,UAAU,EAAE,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,UAAU,EAAE,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAA;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;CAClB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CallerInfo } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the first relevant caller from the current stack trace.
|
|
4
|
+
* Filters out node_modules, Prisma internals, and this library's code.
|
|
5
|
+
*
|
|
6
|
+
* @returns CallerInfo for the first non-library caller, or a fallback if none found
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractCaller(): CallerInfo;
|
|
9
|
+
/**
|
|
10
|
+
* Create a unique key for a caller based on file and line.
|
|
11
|
+
* Used for deduplication in graphs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function callerKey(caller: CallerInfo): string;
|
|
14
|
+
/**
|
|
15
|
+
* Add a caller to an array if it's not already present (deduplicated by file:line).
|
|
16
|
+
* Modifies the array in place.
|
|
17
|
+
*
|
|
18
|
+
* @param callers Array of callers to add to
|
|
19
|
+
* @param caller Caller to add if unique
|
|
20
|
+
*/
|
|
21
|
+
export declare function addCallerIfUnique(callers: CallerInfo[], caller: CallerInfo): void;
|
|
22
|
+
/**
|
|
23
|
+
* Format a CallerInfo as a human-readable string.
|
|
24
|
+
* File paths are made relative to the current working directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatCaller(caller: CallerInfo): string;
|
|
27
|
+
//# sourceMappingURL=caller-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/caller-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AA+F7C;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAmB1C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAMjF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAOvD"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Get the current working directory for relative path calculation
|
|
5
|
+
*/
|
|
6
|
+
function getCwd() {
|
|
7
|
+
// Try to get from process.cwd() first, fall back to the URL of this module
|
|
8
|
+
try {
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Fallback: use this file's location
|
|
13
|
+
const moduleUrl = import.meta.url;
|
|
14
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
15
|
+
return modulePath.slice(0, modulePath.lastIndexOf('/'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Make a file path relative to the current working directory if possible.
|
|
20
|
+
* Returns the original path if it cannot be made relative.
|
|
21
|
+
*/
|
|
22
|
+
function makeRelativePath(filePath) {
|
|
23
|
+
if (filePath === 'unknown') {
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
// Don't try to make non-file paths relative
|
|
27
|
+
if (filePath.startsWith('node:') || filePath.startsWith('(node:')) {
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const cwd = getCwd();
|
|
32
|
+
return relative(cwd, filePath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// If relative path calculation fails, return original
|
|
36
|
+
return filePath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Patterns to filter out from stack traces.
|
|
41
|
+
* These represent library code that should not be considered the "caller"
|
|
42
|
+
*/
|
|
43
|
+
const IGNORE_PATTERNS = [
|
|
44
|
+
/node_modules/,
|
|
45
|
+
/\.prisma[\\/]client/,
|
|
46
|
+
/prisma-deadlock-avoidance-tests[\\/](?:src|dist)[\\/]/,
|
|
47
|
+
/prisma-select-for-update[\\/](?:src|dist)[\\/]/,
|
|
48
|
+
/\(node:/,
|
|
49
|
+
/^node:/,
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Parse a V8 stack frame line and extract location info.
|
|
53
|
+
* Handles both formats:
|
|
54
|
+
* - " at functionName (file:line:column)"
|
|
55
|
+
* - " at file:line:column"
|
|
56
|
+
*/
|
|
57
|
+
function parseStackFrame(line) {
|
|
58
|
+
// Try format: "at functionName (file:line:column)"
|
|
59
|
+
const withFnMatch = line.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/);
|
|
60
|
+
if (withFnMatch) {
|
|
61
|
+
const [, functionName, file, lineStr, columnStr] = withFnMatch;
|
|
62
|
+
return {
|
|
63
|
+
file,
|
|
64
|
+
line: parseInt(lineStr, 10),
|
|
65
|
+
column: parseInt(columnStr, 10),
|
|
66
|
+
functionName: functionName || undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Try format: "at file:line:column"
|
|
70
|
+
const withoutFnMatch = line.match(/at\s+(.+):(\d+):(\d+)/);
|
|
71
|
+
if (withoutFnMatch) {
|
|
72
|
+
const [, file, lineStr, columnStr] = withoutFnMatch;
|
|
73
|
+
return {
|
|
74
|
+
file,
|
|
75
|
+
line: parseInt(lineStr, 10),
|
|
76
|
+
column: parseInt(columnStr, 10),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if a file path should be ignored
|
|
83
|
+
*/
|
|
84
|
+
function shouldIgnore(file) {
|
|
85
|
+
return IGNORE_PATTERNS.some((pattern) => pattern.test(file));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extract the first relevant caller from the current stack trace.
|
|
89
|
+
* Filters out node_modules, Prisma internals, and this library's code.
|
|
90
|
+
*
|
|
91
|
+
* @returns CallerInfo for the first non-library caller, or a fallback if none found
|
|
92
|
+
*/
|
|
93
|
+
export function extractCaller() {
|
|
94
|
+
const stack = new Error().stack ?? '';
|
|
95
|
+
const lines = stack.split('\n').slice(1); // Skip "Error" line
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const parsed = parseStackFrame(line);
|
|
98
|
+
if (!parsed)
|
|
99
|
+
continue;
|
|
100
|
+
if (!shouldIgnore(parsed.file)) {
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Fallback if no relevant frame found
|
|
105
|
+
return {
|
|
106
|
+
file: 'unknown',
|
|
107
|
+
line: 0,
|
|
108
|
+
column: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create a unique key for a caller based on file and line.
|
|
113
|
+
* Used for deduplication in graphs.
|
|
114
|
+
*/
|
|
115
|
+
export function callerKey(caller) {
|
|
116
|
+
return `${caller.file}:${caller.line}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Add a caller to an array if it's not already present (deduplicated by file:line).
|
|
120
|
+
* Modifies the array in place.
|
|
121
|
+
*
|
|
122
|
+
* @param callers Array of callers to add to
|
|
123
|
+
* @param caller Caller to add if unique
|
|
124
|
+
*/
|
|
125
|
+
export function addCallerIfUnique(callers, caller) {
|
|
126
|
+
const key = callerKey(caller);
|
|
127
|
+
const alreadyExists = callers.some((c) => callerKey(c) === key);
|
|
128
|
+
if (!alreadyExists) {
|
|
129
|
+
callers.push(caller);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Format a CallerInfo as a human-readable string.
|
|
134
|
+
* File paths are made relative to the current working directory.
|
|
135
|
+
*/
|
|
136
|
+
export function formatCaller(caller) {
|
|
137
|
+
const relativeFile = makeRelativePath(caller.file);
|
|
138
|
+
const location = `${relativeFile}:${caller.line}:${caller.column}`;
|
|
139
|
+
if (caller.functionName) {
|
|
140
|
+
return `${caller.functionName} (${location})`;
|
|
141
|
+
}
|
|
142
|
+
return location;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=caller-extractor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller-extractor.js","sourceRoot":"","sources":["../../src/utils/caller-extractor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC;;GAEG;AACH,SAAS,MAAM;IACb,2EAA2E;IAC3E,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,GAAG,EAAE,CAAA;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;QACrC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAA;QACjC,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;QAC3C,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;IACzD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,QAAgB;IACxC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,4CAA4C;IAC5C,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClE,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;QACpB,OAAO,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;QACtD,OAAO,QAAQ,CAAA;IACjB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,eAAe,GAAG;IACtB,cAAc;IACd,qBAAqB;IACrB,uDAAuD;IACvD,gDAAgD;IAChD,SAAS;IACT,QAAQ;CACT,CAAA;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,mDAAmD;IACnD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAA;IACnE,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,WAAW,CAAA;QAC9D,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;YAC3B,MAAM,EAAE,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YAC/B,YAAY,EAAE,YAAY,IAAI,SAAS;SACxC,CAAA;IACH,CAAC;IAED,oCAAoC;IACpC,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC1D,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,cAAc,CAAA;QACnD,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;YAC3B,MAAM,EAAE,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;SAChC,CAAA;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA,CAAC,oBAAoB;IAE7D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM;YAAE,SAAQ;QAErB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAA;QACf,CAAC;IACH,CAAC;IAED,sCAAsC;IACtC,OAAO;QACL,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,CAAC;QACP,MAAM,EAAE,CAAC;KACV,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,MAAkB;IAC1C,OAAO,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAA;AACxC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAqB,EAAE,MAAkB;IACzE,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7B,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAA;IAC/D,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC7C,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAClD,MAAM,QAAQ,GAAG,GAAG,YAAY,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,CAAA;IAClE,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO,GAAG,MAAM,CAAC,YAAY,KAAK,QAAQ,GAAG,CAAA;IAC/C,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ModelMeta, ModelField } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get model metadata from Prisma DMMF
|
|
4
|
+
*/
|
|
5
|
+
export declare function getModelMeta(modelName: string): ModelMeta | null;
|
|
6
|
+
/**
|
|
7
|
+
* Get the primary key field(s) for a model
|
|
8
|
+
*/
|
|
9
|
+
export declare function getPrimaryKeyFields(model: ModelMeta): ModelField[];
|
|
10
|
+
/**
|
|
11
|
+
* Extract primary key value from a single result object
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractPrimaryKey(model: ModelMeta, result: Record<string, unknown>): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Extract primary keys from query results (array or single object)
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractPrimaryKeys(modelName: string, result: unknown): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Extract primary keys from a raw result, handling both model field names
|
|
20
|
+
* and database column names
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractPrimaryKeysFromRaw(modelName: string, results: Record<string, unknown>[]): string[];
|
|
23
|
+
//# sourceMappingURL=primary-key-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primary-key-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/primary-key-extractor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExD;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsBhE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,EAAE,CAElE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,GAAG,IAAI,CA4Bf;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,OAAO,GACd,MAAM,EAAE,CAyBV;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,MAAM,EAAE,CA6CV"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Prisma } from '@prisma/client';
|
|
2
|
+
/**
|
|
3
|
+
* Get model metadata from Prisma DMMF
|
|
4
|
+
*/
|
|
5
|
+
export function getModelMeta(modelName) {
|
|
6
|
+
const dmmf = Prisma.dmmf;
|
|
7
|
+
const model = dmmf.datamodel.models.find((m) => m.name.toLowerCase() === modelName.toLowerCase());
|
|
8
|
+
if (!model) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
name: model.name,
|
|
13
|
+
dbName: model.dbName ?? null,
|
|
14
|
+
fields: model.fields.map((f) => ({
|
|
15
|
+
name: f.name,
|
|
16
|
+
kind: f.kind,
|
|
17
|
+
type: f.type,
|
|
18
|
+
dbName: f.dbName ?? null,
|
|
19
|
+
isId: f.isId ?? false,
|
|
20
|
+
isUnique: f.isUnique ?? false,
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the primary key field(s) for a model
|
|
26
|
+
*/
|
|
27
|
+
export function getPrimaryKeyFields(model) {
|
|
28
|
+
return model.fields.filter((f) => f.isId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract primary key value from a single result object
|
|
32
|
+
*/
|
|
33
|
+
export function extractPrimaryKey(model, result) {
|
|
34
|
+
const pkFields = getPrimaryKeyFields(model);
|
|
35
|
+
if (pkFields.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (pkFields.length === 1) {
|
|
39
|
+
// Single primary key
|
|
40
|
+
const field = pkFields[0];
|
|
41
|
+
const value = result[field.name];
|
|
42
|
+
if (value !== undefined && value !== null) {
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Composite primary key - serialize as JSON
|
|
48
|
+
const pkValues = {};
|
|
49
|
+
for (const field of pkFields) {
|
|
50
|
+
const value = result[field.name];
|
|
51
|
+
if (value === undefined || value === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
pkValues[field.name] = value;
|
|
55
|
+
}
|
|
56
|
+
return JSON.stringify(pkValues);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract primary keys from query results (array or single object)
|
|
60
|
+
*/
|
|
61
|
+
export function extractPrimaryKeys(modelName, result) {
|
|
62
|
+
const model = getModelMeta(modelName);
|
|
63
|
+
if (!model) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const keys = [];
|
|
67
|
+
if (Array.isArray(result)) {
|
|
68
|
+
for (const item of result) {
|
|
69
|
+
if (item && typeof item === 'object') {
|
|
70
|
+
const pk = extractPrimaryKey(model, item);
|
|
71
|
+
if (pk !== null) {
|
|
72
|
+
keys.push(pk);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (result && typeof result === 'object') {
|
|
78
|
+
const pk = extractPrimaryKey(model, result);
|
|
79
|
+
if (pk !== null) {
|
|
80
|
+
keys.push(pk);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return keys;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract primary keys from a raw result, handling both model field names
|
|
87
|
+
* and database column names
|
|
88
|
+
*/
|
|
89
|
+
export function extractPrimaryKeysFromRaw(modelName, results) {
|
|
90
|
+
const model = getModelMeta(modelName);
|
|
91
|
+
if (!model) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const pkFields = getPrimaryKeyFields(model);
|
|
95
|
+
if (pkFields.length === 0) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const keys = [];
|
|
99
|
+
for (const result of results) {
|
|
100
|
+
if (pkFields.length === 1) {
|
|
101
|
+
const field = pkFields[0];
|
|
102
|
+
// Try model field name first, then db column name
|
|
103
|
+
const value = result[field.name] ?? (field.dbName ? result[field.dbName] : undefined);
|
|
104
|
+
if (value !== undefined && value !== null) {
|
|
105
|
+
keys.push(String(value));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Composite key
|
|
110
|
+
const pkValues = {};
|
|
111
|
+
let allFound = true;
|
|
112
|
+
for (const field of pkFields) {
|
|
113
|
+
const value = result[field.name] ??
|
|
114
|
+
(field.dbName ? result[field.dbName] : undefined);
|
|
115
|
+
if (value === undefined || value === null) {
|
|
116
|
+
allFound = false;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
pkValues[field.name] = value;
|
|
120
|
+
}
|
|
121
|
+
if (allFound) {
|
|
122
|
+
keys.push(JSON.stringify(pkValues));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return keys;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=primary-key-extractor.js.map
|