prisma-ltree 0.5.0 → 0.7.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/README.md +41 -8
- package/dist/{codec-types-BJE-5cwk.d.mts → codec-types-B0DtEnU6.d.mts} +16 -13
- package/dist/codec-types-B0DtEnU6.d.mts.map +1 -0
- package/dist/codec-types.d.mts +1 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +236 -16
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-2In8FI1I.mjs → descriptor-meta-DwbFei9y.mjs} +341 -34
- package/dist/descriptor-meta-DwbFei9y.mjs.map +1 -0
- package/dist/{operation-types-8su4lbK-.d.mts → operation-types-DD-KyBms.d.mts} +46 -1
- package/dist/{operation-types-8su4lbK-.d.mts.map → operation-types-DD-KyBms.d.mts.map} +1 -1
- package/dist/operation-types.d.mts +1 -1
- package/dist/pack.d.mts +1 -1
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/runtime.d.mts +35 -1
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +34 -1
- package/dist/runtime.mjs.map +1 -1
- package/package.json +11 -10
- package/src/contract.d.ts +87 -20
- package/src/contract.json +22 -1
- package/src/contract.ts +21 -3
- package/src/core/codecs-validation.ts +31 -0
- package/src/core/codecs.ts +32 -55
- package/src/core/contract-space-constants.ts +11 -0
- package/src/core/data-types.ts +36 -2
- package/src/core/descriptor-meta.ts +64 -4
- package/src/core/hierarchy-collection-methods.ts +111 -0
- package/src/core/pattern-validation.ts +313 -0
- package/src/exports/control.ts +36 -1
- package/src/exports/runtime.ts +3 -0
- package/src/types/operation-types.ts +37 -0
- package/dist/codec-types-BJE-5cwk.d.mts.map +0 -1
- package/dist/descriptor-meta-2In8FI1I.mjs.map +0 -1
package/src/core/codecs.ts
CHANGED
|
@@ -1,54 +1,24 @@
|
|
|
1
|
-
import type { JsonValue } from "@prisma/orm-framework/contract/types";
|
|
2
1
|
import {
|
|
2
|
+
type AnyCodecDescriptor,
|
|
3
3
|
type CodecCallContext,
|
|
4
4
|
CodecImpl,
|
|
5
5
|
type CodecInstanceContext,
|
|
6
6
|
type ColumnHelperFor,
|
|
7
7
|
type ColumnHelperForStrict,
|
|
8
8
|
column,
|
|
9
|
+
type DataTypeValue,
|
|
9
10
|
} from "@prisma/orm-framework/components/codec";
|
|
10
11
|
import type { ExtractCodecTypes, ProjectionExpr } from "@prisma/orm-family-sql/relational-core/ast";
|
|
12
|
+
import { CastExpr } from "@prisma/orm-family-sql/relational-core/ast";
|
|
11
13
|
import {
|
|
12
14
|
definePostgresCodecs,
|
|
13
15
|
PostgresCodecDescriptor,
|
|
14
16
|
} from "@prisma/orm-target-postgres/target/codec-descriptor";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
LTREE_CODEC_ID,
|
|
18
|
-
LTREE_MAX_LABEL_LENGTH,
|
|
19
|
-
LTREE_MAX_LABELS,
|
|
20
|
-
} from "./constants";
|
|
17
|
+
import { assertValidLtree } from "./codecs-validation";
|
|
18
|
+
import { LTREE_ARRAY_CODEC_ID, LTREE_CODEC_ID } from "./constants";
|
|
21
19
|
import { ltreeArrayDataType, ltreeDataType } from "./data-types";
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export function assertValidLtree(value: unknown): asserts value is string {
|
|
26
|
-
if (typeof value !== "string") {
|
|
27
|
-
throw new Error("ltree value must be a string");
|
|
28
|
-
}
|
|
29
|
-
if (value.length === 0) {
|
|
30
|
-
throw new Error("ltree value must not be empty");
|
|
31
|
-
}
|
|
32
|
-
const labels = value.split(".");
|
|
33
|
-
if (labels.length > LTREE_MAX_LABELS) {
|
|
34
|
-
throw new Error(`ltree path exceeds max labels: got ${labels.length}, max ${LTREE_MAX_LABELS}`);
|
|
35
|
-
}
|
|
36
|
-
for (const label of labels) {
|
|
37
|
-
if (label.length === 0) {
|
|
38
|
-
throw new Error(`ltree label must not be empty in path "${value}"`);
|
|
39
|
-
}
|
|
40
|
-
if (label.length > LTREE_MAX_LABEL_LENGTH) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
`ltree label exceeds max length: got ${label.length}, max ${LTREE_MAX_LABEL_LENGTH}`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (!LABEL_PATTERN.test(label)) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`ltree label "${label}" contains invalid characters; allowed: alphanumeric, underscore, hyphen`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
21
|
+
export { assertValidLtree } from "./codecs-validation";
|
|
52
22
|
|
|
53
23
|
export class LtreeCodec extends CodecImpl<
|
|
54
24
|
typeof LTREE_CODEC_ID,
|
|
@@ -56,30 +26,34 @@ export class LtreeCodec extends CodecImpl<
|
|
|
56
26
|
string,
|
|
57
27
|
string
|
|
58
28
|
> {
|
|
59
|
-
|
|
29
|
+
constructor(descriptor: AnyCodecDescriptor) {
|
|
30
|
+
super(descriptor, ltreeDataType);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async toWire(value: string, _ctx: CodecCallContext): Promise<string> {
|
|
60
34
|
assertValidLtree(value);
|
|
61
35
|
return value;
|
|
62
36
|
}
|
|
63
37
|
|
|
64
|
-
async
|
|
38
|
+
async fromWire(wire: string, _ctx: CodecCallContext): Promise<string> {
|
|
65
39
|
return wire;
|
|
66
40
|
}
|
|
67
41
|
|
|
68
|
-
|
|
42
|
+
toDataTypeValue(value: string): DataTypeValue {
|
|
69
43
|
assertValidLtree(value);
|
|
70
|
-
return value;
|
|
44
|
+
return this.dataTypeValueOf(value);
|
|
71
45
|
}
|
|
72
46
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return json;
|
|
47
|
+
fromDataTypeValue(value: DataTypeValue): string {
|
|
48
|
+
return value.value as string;
|
|
76
49
|
}
|
|
77
50
|
}
|
|
78
51
|
|
|
79
52
|
export class LtreeDescriptor extends PostgresCodecDescriptor<void> {
|
|
53
|
+
override readonly jsonType = "string" as const;
|
|
80
54
|
protected override jsonProjection(expression: ProjectionExpr): ProjectionExpr {
|
|
81
|
-
//
|
|
82
|
-
return expression;
|
|
55
|
+
// JSON projections read through fromWire; cast so the wire value is text.
|
|
56
|
+
return CastExpr.as(expression, "text");
|
|
83
57
|
}
|
|
84
58
|
override readonly dataType = ltreeDataType.id;
|
|
85
59
|
override readonly codecId = LTREE_CODEC_ID;
|
|
@@ -107,38 +81,41 @@ export class LtreeArrayCodec extends CodecImpl<
|
|
|
107
81
|
readonly string[],
|
|
108
82
|
readonly string[]
|
|
109
83
|
> {
|
|
110
|
-
|
|
84
|
+
constructor(descriptor: AnyCodecDescriptor) {
|
|
85
|
+
super(descriptor, ltreeArrayDataType);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async toWire(value: readonly string[], _ctx: CodecCallContext): Promise<readonly string[]> {
|
|
111
89
|
for (const entry of value) {
|
|
112
90
|
assertValidLtree(entry);
|
|
113
91
|
}
|
|
114
92
|
return value;
|
|
115
93
|
}
|
|
116
94
|
|
|
117
|
-
async
|
|
95
|
+
async fromWire(wire: readonly string[], _ctx: CodecCallContext): Promise<readonly string[]> {
|
|
118
96
|
return wire;
|
|
119
97
|
}
|
|
120
98
|
|
|
121
|
-
|
|
99
|
+
toDataTypeValue(value: readonly string[]): DataTypeValue {
|
|
122
100
|
for (const entry of value) {
|
|
123
101
|
assertValidLtree(entry);
|
|
124
102
|
}
|
|
125
|
-
return [...value];
|
|
103
|
+
return this.dataTypeValueOf([...value]);
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
|
|
106
|
+
fromDataTypeValue(value: DataTypeValue): readonly string[] {
|
|
107
|
+
const json = value.value;
|
|
129
108
|
if (!Array.isArray(json)) {
|
|
130
109
|
return [];
|
|
131
110
|
}
|
|
132
|
-
return json.map((entry) =>
|
|
133
|
-
assertValidLtree(entry);
|
|
134
|
-
return String(entry);
|
|
135
|
-
});
|
|
111
|
+
return json.map((entry) => String(entry));
|
|
136
112
|
}
|
|
137
113
|
}
|
|
138
114
|
|
|
139
115
|
export class LtreeArrayDescriptor extends PostgresCodecDescriptor<void> {
|
|
116
|
+
override readonly jsonType = "array" as const;
|
|
140
117
|
protected override jsonProjection(expression: ProjectionExpr): ProjectionExpr {
|
|
141
|
-
// ltree[]
|
|
118
|
+
// ltree[] already projects as a JSON string array for fromWire.
|
|
142
119
|
return expression;
|
|
143
120
|
}
|
|
144
121
|
override readonly dataType = ltreeArrayDataType.id;
|
|
@@ -7,6 +7,17 @@ export const LTREE_ARRAY_STORAGE_TYPE = "ltreeArray" as const;
|
|
|
7
7
|
|
|
8
8
|
export const LTREE_BASELINE_MIGRATION_NAME = "20260619T2142_install_ltree" as const;
|
|
9
9
|
|
|
10
|
+
export const LTREE_DECLARE_EXTENSION_MIGRATION_NAME =
|
|
11
|
+
"20261111T1905_declare_ltree_extension" as const;
|
|
12
|
+
|
|
13
|
+
export const LTREE_CREATE_EXTENSION_MIGRATION_NAME =
|
|
14
|
+
"20261111T1904_create_ltree_extension" as const;
|
|
15
|
+
|
|
16
|
+
export const LTREE_SEARCH_PATH_MIGRATION_NAME = "20261111T1908_find_ltree_on_search_path" as const;
|
|
17
|
+
|
|
18
|
+
export const LTREE_SEARCH_PATH_INSTALL_MIGRATION_NAME =
|
|
19
|
+
"20261111T1908_install_ltree_on_search_path" as const;
|
|
20
|
+
|
|
10
21
|
export const LTREE_INVARIANTS = {
|
|
11
22
|
installLtree: "ltree:install-ltree-v1",
|
|
12
23
|
} as const;
|
package/src/core/data-types.ts
CHANGED
|
@@ -2,28 +2,62 @@
|
|
|
2
2
|
* Data types this extension owns.
|
|
3
3
|
*
|
|
4
4
|
* `ltree` and `ltree[]` are PostgreSQL types the target does not register.
|
|
5
|
-
* A path's
|
|
5
|
+
* A path's stored form is its label text. An array's stored form is a
|
|
6
6
|
* JSON array of those texts. Both accept text the contract can already write.
|
|
7
7
|
* The texts are what a migration writes and what the catalog reports.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { DataType } from "@prisma/orm-framework/components/codec";
|
|
10
|
+
import type { DataType, DataTypeReader } from "@prisma/orm-framework/components/codec";
|
|
11
|
+
import { refuseJsonValue } from "@prisma/orm-framework/components/codec";
|
|
11
12
|
import { sqlDataType } from "@prisma/orm-family-sql/contract/data-type";
|
|
12
13
|
import { pgText } from "@prisma/orm-target-postgres/target/data-types";
|
|
14
|
+
import { assertValidLtree } from "./codecs-validation";
|
|
15
|
+
|
|
16
|
+
const readLtree: DataTypeReader = (json) => {
|
|
17
|
+
if (typeof json !== "string") {
|
|
18
|
+
return refuseJsonValue("ltree/ltree", "an ltree path string", json);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
assertValidLtree(json);
|
|
22
|
+
} catch {
|
|
23
|
+
return refuseJsonValue("ltree/ltree", "a valid ltree path string", json);
|
|
24
|
+
}
|
|
25
|
+
return json;
|
|
26
|
+
};
|
|
13
27
|
|
|
14
28
|
export const ltreeDataType = sqlDataType("ltree/ltree", {
|
|
29
|
+
read: readLtree,
|
|
15
30
|
texts: [{ text: "ltree", written: true, catalog: true }],
|
|
16
31
|
casts: {
|
|
17
32
|
[pgText.id]: (value) => value,
|
|
18
33
|
},
|
|
19
34
|
});
|
|
20
35
|
|
|
36
|
+
const readLtreeArray: DataTypeReader = (json) => {
|
|
37
|
+
const expected = "an array of ltree path strings";
|
|
38
|
+
if (!Array.isArray(json)) {
|
|
39
|
+
return refuseJsonValue("ltree/ltree-array", expected, json);
|
|
40
|
+
}
|
|
41
|
+
for (const entry of json) {
|
|
42
|
+
if (typeof entry !== "string") {
|
|
43
|
+
return refuseJsonValue("ltree/ltree-array", expected, entry);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
assertValidLtree(entry);
|
|
47
|
+
} catch {
|
|
48
|
+
return refuseJsonValue("ltree/ltree-array", expected, entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return json;
|
|
52
|
+
};
|
|
53
|
+
|
|
21
54
|
/**
|
|
22
55
|
* `sqlDataType` accepts parentheses in a type name and refuses square brackets.
|
|
23
56
|
* PostgreSQL writes this type as `ltree[]`, and `format_type` reports `ltree[]`.
|
|
24
57
|
* Declare the type first, then set that text so a migration and a catalog check use it.
|
|
25
58
|
*/
|
|
26
59
|
const ltreeArrayDeclared = sqlDataType("ltree/ltree-array", {
|
|
60
|
+
read: readLtreeArray,
|
|
27
61
|
listCast: {
|
|
28
62
|
of: [pgText.id],
|
|
29
63
|
cast: (elements) => [...elements],
|
|
@@ -11,6 +11,7 @@ import type { QueryOperationTypes } from "../types/operation-types";
|
|
|
11
11
|
import { ltreeAuthoringTypes } from "./authoring";
|
|
12
12
|
import { LTREE_ARRAY_CODEC_ID, LTREE_CODEC_ID } from "./constants";
|
|
13
13
|
import { ltreeDataTypes } from "./data-types";
|
|
14
|
+
import { assertPatternArg } from "./pattern-validation";
|
|
14
15
|
import { ltreeCodecRegistry } from "./registry";
|
|
15
16
|
|
|
16
17
|
type CodecTypesBase = Record<string, { readonly input: unknown; readonly output: unknown }>;
|
|
@@ -57,8 +58,10 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
57
58
|
// Hierarchy operators compare two `ltree` values; the user argument shares the
|
|
58
59
|
// column's `pg/ltree@1` codec. Pattern-match operators take a query/pattern
|
|
59
60
|
// string (or `string[]`) that is NOT an ltree — it is bound as text and cast
|
|
60
|
-
// in-template to `lquery` / `lquery[]` / `ltxtquery
|
|
61
|
-
//
|
|
61
|
+
// in-template to `lquery` / `lquery[]` / `ltxtquery`. Literal string args get a
|
|
62
|
+
// lightweight structural check first (see `pattern-validation.ts`) so empty /
|
|
63
|
+
// obviously illegal patterns fail with a clear pack error instead of an opaque
|
|
64
|
+
// Postgres cast failure; Expression / ParamRef binds still defer to Postgres.
|
|
62
65
|
const hierarchyOp = (
|
|
63
66
|
method: string,
|
|
64
67
|
operator: string,
|
|
@@ -80,11 +83,12 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
80
83
|
const patternOp = (
|
|
81
84
|
method: string,
|
|
82
85
|
operator: string,
|
|
83
|
-
castType:
|
|
86
|
+
castType: "lquery" | "ltxtquery" | "lquery[]",
|
|
84
87
|
self: CodecExpression<typeof LTREE_CODEC_ID, boolean, CT>,
|
|
85
88
|
arg: unknown,
|
|
86
89
|
many = false,
|
|
87
90
|
): BoolReturn => {
|
|
91
|
+
assertPatternArg(castType, arg);
|
|
88
92
|
const selfCodec = codecOf(self);
|
|
89
93
|
return buildOperation({
|
|
90
94
|
method,
|
|
@@ -118,6 +122,34 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
118
122
|
lowering: { targetFamily: "sql", template },
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
// Boolean operators on an `ltree[]` receiver (ADR-007). Same bind/cast rules as
|
|
126
|
+
// `firstMatchOp`, but the result is `boolean`. These are the operators
|
|
127
|
+
// `gist__ltree_ops` indexes (`<@`, `~`, `@`, `?`), plus `@>` which PostgreSQL
|
|
128
|
+
// ships and the opclass does not index.
|
|
129
|
+
const arrayBoolOp = (
|
|
130
|
+
method: string,
|
|
131
|
+
operator: string,
|
|
132
|
+
self: CodecExpression<typeof LTREE_ARRAY_CODEC_ID, boolean, CT>,
|
|
133
|
+
arg: unknown,
|
|
134
|
+
argCodecId: typeof LTREE_CODEC_ID | typeof TEXT_CODEC_ID,
|
|
135
|
+
castType?: "lquery" | "ltxtquery" | "lquery[]",
|
|
136
|
+
many = false,
|
|
137
|
+
): BoolReturn => {
|
|
138
|
+
if (castType) assertPatternArg(castType, arg);
|
|
139
|
+
const template = castType
|
|
140
|
+
? `{{self}} ${operator} ({{arg0}})::${castType}`
|
|
141
|
+
: `{{self}} ${operator} {{arg0}}`;
|
|
142
|
+
return buildOperation({
|
|
143
|
+
method,
|
|
144
|
+
args: [
|
|
145
|
+
toExpr(self, codecOf(self)),
|
|
146
|
+
toExpr(arg, many ? { codecId: argCodecId, many: true } : { codecId: argCodecId }),
|
|
147
|
+
],
|
|
148
|
+
returns: BOOL_RETURN,
|
|
149
|
+
lowering: { targetFamily: "sql", template },
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
121
153
|
// Tier 3 — first-match operators on an `ltree[]` receiver (ADR-003).
|
|
122
154
|
const firstMatchOp = (
|
|
123
155
|
method: string,
|
|
@@ -125,8 +157,9 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
125
157
|
self: CodecExpression<typeof LTREE_ARRAY_CODEC_ID, boolean, CT>,
|
|
126
158
|
arg: unknown,
|
|
127
159
|
argCodecId: typeof LTREE_CODEC_ID | typeof TEXT_CODEC_ID,
|
|
128
|
-
castType?:
|
|
160
|
+
castType?: "lquery" | "ltxtquery",
|
|
129
161
|
): LtreeReturn => {
|
|
162
|
+
if (castType) assertPatternArg(castType, arg);
|
|
130
163
|
const selfCodec = codecOf(self);
|
|
131
164
|
const template = castType
|
|
132
165
|
? `{{self}} ${operator} ({{arg0}})::${castType}`
|
|
@@ -281,6 +314,33 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
281
314
|
impl: (self, query) =>
|
|
282
315
|
firstMatchOp("firstMatchLtxtquery", "?@", self, query, TEXT_CODEC_ID, "ltxtquery"),
|
|
283
316
|
},
|
|
317
|
+
// Boolean array operators (ADR-007). Distinct names from scalar methods because
|
|
318
|
+
// the operation registry keys by name only. `containsDescendantOf`,
|
|
319
|
+
// `matchesAnyLquery`, `matchesAnyLtxtquery`, and `matchesAnyLqueryArray` are
|
|
320
|
+
// in `gist__ltree_ops`; `containsAncestorOf` is not.
|
|
321
|
+
containsAncestorOf: {
|
|
322
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
323
|
+
impl: (self, other) => arrayBoolOp("containsAncestorOf", "@>", self, other, LTREE_CODEC_ID),
|
|
324
|
+
},
|
|
325
|
+
containsDescendantOf: {
|
|
326
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
327
|
+
impl: (self, other) => arrayBoolOp("containsDescendantOf", "<@", self, other, LTREE_CODEC_ID),
|
|
328
|
+
},
|
|
329
|
+
matchesAnyLquery: {
|
|
330
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
331
|
+
impl: (self, pattern) =>
|
|
332
|
+
arrayBoolOp("matchesAnyLquery", "~", self, pattern, TEXT_CODEC_ID, "lquery"),
|
|
333
|
+
},
|
|
334
|
+
matchesAnyLqueryArray: {
|
|
335
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
336
|
+
impl: (self, patterns) =>
|
|
337
|
+
arrayBoolOp("matchesAnyLqueryArray", "?", self, patterns, TEXT_CODEC_ID, "lquery[]", true),
|
|
338
|
+
},
|
|
339
|
+
matchesAnyLtxtquery: {
|
|
340
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
341
|
+
impl: (self, query) =>
|
|
342
|
+
arrayBoolOp("matchesAnyLtxtquery", "@", self, query, TEXT_CODEC_ID, "ltxtquery"),
|
|
343
|
+
},
|
|
284
344
|
// `lca(ltree[])` — array-receiver form (ADR-001, ADR-005). Named `lcaAll`
|
|
285
345
|
// because prisma-next keys operations by name only (`createOperationRegistry`
|
|
286
346
|
// throws on a duplicate); `lca` is already the variadic scalar method on
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectionMethodBody,
|
|
3
|
+
CollectionMethodContribution,
|
|
4
|
+
CollectionMethodContributionId,
|
|
5
|
+
CollectionMethodContributionType,
|
|
6
|
+
CollectionMethodModelType,
|
|
7
|
+
} from "@prisma/orm-family-sql/relational-core/collection-methods";
|
|
8
|
+
import { blindCast } from "@prisma/orm-framework/utils/casts";
|
|
9
|
+
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
10
|
+
import type { Ltree } from "../types/codec-types";
|
|
11
|
+
import { LTREE_CODEC_ID } from "./constants";
|
|
12
|
+
|
|
13
|
+
type LtreeFieldNames<Fields> = {
|
|
14
|
+
[Name in keyof Fields]: Fields[Name] extends { readonly many: object }
|
|
15
|
+
? never
|
|
16
|
+
: Fields[Name] extends {
|
|
17
|
+
readonly type: { readonly codecId: typeof LTREE_CODEC_ID };
|
|
18
|
+
}
|
|
19
|
+
? Name
|
|
20
|
+
: never;
|
|
21
|
+
}[keyof Fields];
|
|
22
|
+
|
|
23
|
+
type HierarchyMethodTable<Model extends CollectionMethodModelType> = {
|
|
24
|
+
readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_descendants`]: {
|
|
25
|
+
readonly parameters: readonly [root: Ltree];
|
|
26
|
+
readonly filters: true;
|
|
27
|
+
readonly orders: false;
|
|
28
|
+
};
|
|
29
|
+
} & {
|
|
30
|
+
readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_ancestors`]: {
|
|
31
|
+
readonly parameters: readonly [leaf: Ltree];
|
|
32
|
+
readonly filters: true;
|
|
33
|
+
readonly orders: false;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One `{field}_descendants` / `{field}_ancestors` method per scalar `ltree`
|
|
39
|
+
* field. Descendants map to `isDescendantOf` (`<@`); ancestors map to
|
|
40
|
+
* `isAncestorOf` (`@>`). Both are inclusive of equality, matching PostgreSQL.
|
|
41
|
+
*/
|
|
42
|
+
export interface LtreeHierarchyMethods extends CollectionMethodContributionType {
|
|
43
|
+
readonly methods: HierarchyMethodTable<this["model"]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare module "@prisma/orm-family-sql/relational-core/collection-methods" {
|
|
47
|
+
interface CollectionMethodRegistry {
|
|
48
|
+
readonly "ltree/hierarchy": LtreeHierarchyMethods;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface HierarchyField {
|
|
53
|
+
isDescendantOf(root: Ltree): unknown;
|
|
54
|
+
isAncestorOf(leaf: Ltree): unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type LtreeFields = Readonly<Record<string, HierarchyField>>;
|
|
58
|
+
|
|
59
|
+
function fieldOf(model: unknown, field: string): HierarchyField {
|
|
60
|
+
const accessor = blindCast<
|
|
61
|
+
LtreeFields,
|
|
62
|
+
"an ltree field of the ORM model accessor has hierarchy operators"
|
|
63
|
+
>(model)[field];
|
|
64
|
+
if (accessor === undefined) {
|
|
65
|
+
throw new InternalError(`The model accessor has no ltree field "${field}".`);
|
|
66
|
+
}
|
|
67
|
+
return accessor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function descendantsMethod(field: string): CollectionMethodBody {
|
|
71
|
+
return (collection, ...args) => {
|
|
72
|
+
const [root] = blindCast<readonly [Ltree], "the type half declares a root path">(args);
|
|
73
|
+
return collection.where((model) => fieldOf(model, field).isDescendantOf(root));
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ancestorsMethod(field: string): CollectionMethodBody {
|
|
78
|
+
return (collection, ...args) => {
|
|
79
|
+
const [leaf] = blindCast<readonly [Ltree], "the type half declares a leaf path">(args);
|
|
80
|
+
return collection.where((model) => fieldOf(model, field).isAncestorOf(leaf));
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isScalarLtreeField(field: unknown): boolean {
|
|
85
|
+
if (typeof field !== "object" || field === null) return false;
|
|
86
|
+
const record = field as {
|
|
87
|
+
readonly many?: unknown;
|
|
88
|
+
readonly type?: { readonly kind?: string; readonly codecId?: string };
|
|
89
|
+
};
|
|
90
|
+
return (
|
|
91
|
+
record.many === undefined &&
|
|
92
|
+
record.type?.kind === "scalar" &&
|
|
93
|
+
record.type.codecId === LTREE_CODEC_ID
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The run-time half of `LtreeHierarchyMethods`. */
|
|
98
|
+
export const ltreeHierarchyMethods: CollectionMethodContribution = {
|
|
99
|
+
id: "ltree/hierarchy" satisfies CollectionMethodContributionId,
|
|
100
|
+
methodsFor: (model) =>
|
|
101
|
+
Object.fromEntries(
|
|
102
|
+
Object.entries(model.fields).flatMap(([name, field]) =>
|
|
103
|
+
isScalarLtreeField(field)
|
|
104
|
+
? [
|
|
105
|
+
[`${name}_descendants`, descendantsMethod(name)],
|
|
106
|
+
[`${name}_ancestors`, ancestorsMethod(name)],
|
|
107
|
+
]
|
|
108
|
+
: [],
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
};
|