graphile-ltree 1.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 +23 -0
- package/README.md +138 -0
- package/esm/index.d.ts +24 -0
- package/esm/index.js +25 -0
- package/esm/plugins/connection-filter-operators.d.ts +18 -0
- package/esm/plugins/connection-filter-operators.js +97 -0
- package/esm/plugins/detect-ltree.d.ts +18 -0
- package/esm/plugins/detect-ltree.js +52 -0
- package/esm/plugins/folder-field.d.ts +19 -0
- package/esm/plugins/folder-field.js +102 -0
- package/esm/plugins/folder-filter-operators.d.ts +23 -0
- package/esm/plugins/folder-filter-operators.js +99 -0
- package/esm/plugins/ltree-codec.d.ts +16 -0
- package/esm/plugins/ltree-codec.js +121 -0
- package/esm/preset.d.ts +28 -0
- package/esm/preset.js +52 -0
- package/index.d.ts +24 -0
- package/index.js +36 -0
- package/package.json +67 -0
- package/plugins/connection-filter-operators.d.ts +18 -0
- package/plugins/connection-filter-operators.js +103 -0
- package/plugins/detect-ltree.d.ts +18 -0
- package/plugins/detect-ltree.js +55 -0
- package/plugins/folder-field.d.ts +19 -0
- package/plugins/folder-field.js +105 -0
- package/plugins/folder-filter-operators.d.ts +23 -0
- package/plugins/folder-filter-operators.js +105 -0
- package/plugins/ltree-codec.d.ts +16 -0
- package/plugins/ltree-codec.js +127 -0
- package/preset.d.ts +28 -0
- package/preset.js +55 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LtreeCodecPlugin
|
|
3
|
+
*
|
|
4
|
+
* Ensures PostGraphile v5 properly handles ltree, lquery, and ltxtquery types.
|
|
5
|
+
*
|
|
6
|
+
* PostGraphile v5 (rc.8+) natively maps ltree to a GraphQL scalar named "LTree".
|
|
7
|
+
* This plugin:
|
|
8
|
+
* 1. Falls back to registering codecs if PostGraphile's native handling misses them
|
|
9
|
+
* 2. Re-enables ltree columns for select/filterBy (overrides rc.8 HIDE_BY_DEFAULT)
|
|
10
|
+
*
|
|
11
|
+
* The native scalar name is "LTree" (capital T). All operators and downstream
|
|
12
|
+
* plugins should reference LTREE_SCALAR_NAME for consistency.
|
|
13
|
+
*/
|
|
14
|
+
import { GraphQLString } from 'graphql';
|
|
15
|
+
import sql from 'pg-sql2';
|
|
16
|
+
export const LTREE_SCALAR_NAME = 'LTree';
|
|
17
|
+
export const LtreeCodecPlugin = {
|
|
18
|
+
name: 'LtreeCodecPlugin',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
description: 'Ensures ltree/lquery codecs are properly registered and enabled',
|
|
21
|
+
gather: {
|
|
22
|
+
hooks: {
|
|
23
|
+
async pgCodecs_findPgCodec(info, event) {
|
|
24
|
+
if (event.pgCodec)
|
|
25
|
+
return;
|
|
26
|
+
const { pgType: type, serviceName } = event;
|
|
27
|
+
const isLtree = type.typname === 'ltree';
|
|
28
|
+
const isLquery = type.typname === 'lquery';
|
|
29
|
+
const isLtxtquery = type.typname === 'ltxtquery';
|
|
30
|
+
if (!isLtree && !isLquery && !isLtxtquery)
|
|
31
|
+
return;
|
|
32
|
+
const ns = await info.helpers.pgIntrospection.getNamespace(serviceName, type.typnamespace);
|
|
33
|
+
const schemaName = ns?.nspname || 'pg_catalog';
|
|
34
|
+
event.pgCodec = {
|
|
35
|
+
name: type.typname,
|
|
36
|
+
sqlType: sql.identifier(schemaName, type.typname),
|
|
37
|
+
fromPg: (value) => value,
|
|
38
|
+
toPg: (value) => value,
|
|
39
|
+
attributes: undefined,
|
|
40
|
+
executor: null,
|
|
41
|
+
extensions: {
|
|
42
|
+
oid: type._id,
|
|
43
|
+
pg: {
|
|
44
|
+
serviceName,
|
|
45
|
+
schemaName,
|
|
46
|
+
name: type.typname
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
schema: {
|
|
54
|
+
hooks: {
|
|
55
|
+
init: {
|
|
56
|
+
before: ['PgCodecs', 'PgConnectionArgFilterPlugin'],
|
|
57
|
+
callback(_, build) {
|
|
58
|
+
const { setGraphQLTypeForPgCodec, hasGraphQLTypeForPgCodec } = build;
|
|
59
|
+
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
|
|
60
|
+
if (codec.name === 'ltree') {
|
|
61
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'input')) {
|
|
62
|
+
build.registerScalarType(LTREE_SCALAR_NAME, {}, () => ({
|
|
63
|
+
description: 'A PostgreSQL ltree hierarchical label path, represented as a dot-delimited string (e.g. "projects.alpha.docs").',
|
|
64
|
+
serialize(value) {
|
|
65
|
+
return String(value);
|
|
66
|
+
},
|
|
67
|
+
parseValue(value) {
|
|
68
|
+
if (typeof value === 'string')
|
|
69
|
+
return value;
|
|
70
|
+
throw new Error(`${LTREE_SCALAR_NAME} must be a string`);
|
|
71
|
+
},
|
|
72
|
+
parseLiteral(lit) {
|
|
73
|
+
if (lit.kind === 'NullValue')
|
|
74
|
+
return null;
|
|
75
|
+
if (lit.kind !== 'StringValue') {
|
|
76
|
+
throw new Error(`${LTREE_SCALAR_NAME} must be a string`);
|
|
77
|
+
}
|
|
78
|
+
return lit.value;
|
|
79
|
+
}
|
|
80
|
+
}), `LtreeCodecPlugin registering ${LTREE_SCALAR_NAME} scalar`);
|
|
81
|
+
setGraphQLTypeForPgCodec(codec, 'input', LTREE_SCALAR_NAME);
|
|
82
|
+
setGraphQLTypeForPgCodec(codec, 'output', LTREE_SCALAR_NAME);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (codec.name === 'lquery' || codec.name === 'ltxtquery') {
|
|
86
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'input')) {
|
|
87
|
+
setGraphQLTypeForPgCodec(codec, 'input', GraphQLString.name);
|
|
88
|
+
}
|
|
89
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'output')) {
|
|
90
|
+
setGraphQLTypeForPgCodec(codec, 'output', GraphQLString.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return _;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
entityBehavior: {
|
|
99
|
+
pgCodecAttribute: {
|
|
100
|
+
inferred: {
|
|
101
|
+
after: ['default'],
|
|
102
|
+
provides: ['ltreeCodecPlugin'],
|
|
103
|
+
callback(behavior, [codec, attributeName]) {
|
|
104
|
+
const attr = codec.attributes?.[attributeName];
|
|
105
|
+
const attrCodec = attr?.codec;
|
|
106
|
+
const isLtree = attrCodec?.name === 'ltree' ||
|
|
107
|
+
attrCodec?.extensions?.pg?.name === 'ltree';
|
|
108
|
+
if (!isLtree)
|
|
109
|
+
return behavior;
|
|
110
|
+
return [
|
|
111
|
+
behavior,
|
|
112
|
+
'attribute:select',
|
|
113
|
+
'attribute:base',
|
|
114
|
+
'condition:attribute:filterBy'
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
2
|
+
/**
|
|
3
|
+
* GraphileLtreePreset
|
|
4
|
+
*
|
|
5
|
+
* Base preset: ltree codec, detection, and raw ltree operators.
|
|
6
|
+
*
|
|
7
|
+
* Operators on LTree fields: isAncestorOf, isDescendantOf, matchesGlob
|
|
8
|
+
* (accept dot-delimited or slash-delimited paths)
|
|
9
|
+
*/
|
|
10
|
+
export declare const GraphileLtreePreset: GraphileConfig.Preset;
|
|
11
|
+
/**
|
|
12
|
+
* GraphileFolderPreset
|
|
13
|
+
*
|
|
14
|
+
* Folder-oriented layer on top of the base ltree preset.
|
|
15
|
+
*
|
|
16
|
+
* Adds:
|
|
17
|
+
* - Virtual `{column}Folder` fields with slash-delimited paths
|
|
18
|
+
* - Folder operators: within, ancestorOf, glob (always slash-delimited)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```graphql
|
|
22
|
+
* allFiles(where: { path: { within: "/projects/alpha" } }) {
|
|
23
|
+
* nodes { pathFolder filename }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const GraphileFolderPreset: GraphileConfig.Preset;
|
|
28
|
+
export default GraphileFolderPreset;
|
package/esm/preset.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createLtreeOperatorFactory } from './plugins/connection-filter-operators';
|
|
2
|
+
import { LtreeExtensionDetectionPlugin } from './plugins/detect-ltree';
|
|
3
|
+
import { LtreeFolderFieldPlugin } from './plugins/folder-field';
|
|
4
|
+
import { createFolderOperatorFactory } from './plugins/folder-filter-operators';
|
|
5
|
+
import { LtreeCodecPlugin } from './plugins/ltree-codec';
|
|
6
|
+
/**
|
|
7
|
+
* GraphileLtreePreset
|
|
8
|
+
*
|
|
9
|
+
* Base preset: ltree codec, detection, and raw ltree operators.
|
|
10
|
+
*
|
|
11
|
+
* Operators on LTree fields: isAncestorOf, isDescendantOf, matchesGlob
|
|
12
|
+
* (accept dot-delimited or slash-delimited paths)
|
|
13
|
+
*/
|
|
14
|
+
export const GraphileLtreePreset = {
|
|
15
|
+
plugins: [
|
|
16
|
+
LtreeExtensionDetectionPlugin,
|
|
17
|
+
LtreeCodecPlugin
|
|
18
|
+
],
|
|
19
|
+
schema: {
|
|
20
|
+
connectionFilterOperatorFactories: [
|
|
21
|
+
createLtreeOperatorFactory()
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* GraphileFolderPreset
|
|
27
|
+
*
|
|
28
|
+
* Folder-oriented layer on top of the base ltree preset.
|
|
29
|
+
*
|
|
30
|
+
* Adds:
|
|
31
|
+
* - Virtual `{column}Folder` fields with slash-delimited paths
|
|
32
|
+
* - Folder operators: within, ancestorOf, glob (always slash-delimited)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```graphql
|
|
36
|
+
* allFiles(where: { path: { within: "/projects/alpha" } }) {
|
|
37
|
+
* nodes { pathFolder filename }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export const GraphileFolderPreset = {
|
|
42
|
+
extends: [GraphileLtreePreset],
|
|
43
|
+
plugins: [
|
|
44
|
+
LtreeFolderFieldPlugin
|
|
45
|
+
],
|
|
46
|
+
schema: {
|
|
47
|
+
connectionFilterOperatorFactories: [
|
|
48
|
+
createFolderOperatorFactory()
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export default GraphileFolderPreset;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphile-ltree — PostGraphile v5 ltree Plugin
|
|
3
|
+
*
|
|
4
|
+
* Two presets:
|
|
5
|
+
*
|
|
6
|
+
* - `GraphileLtreePreset` — base ltree operators (isAncestorOf, isDescendantOf, matchesGlob)
|
|
7
|
+
* - `GraphileFolderPreset` — folder-oriented layer (within, ancestorOf, glob) + pathFolder fields
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // For folder-style interface (recommended):
|
|
12
|
+
* import { GraphileFolderPreset } from 'graphile-ltree';
|
|
13
|
+
*
|
|
14
|
+
* // For raw ltree operators only:
|
|
15
|
+
* import { GraphileLtreePreset } from 'graphile-ltree';
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export { GraphileFolderPreset, GraphileLtreePreset } from './preset';
|
|
19
|
+
export type { LtreeExtensionInfo } from './plugins/detect-ltree';
|
|
20
|
+
export { LtreeExtensionDetectionPlugin } from './plugins/detect-ltree';
|
|
21
|
+
export { LtreeFolderFieldPlugin } from './plugins/folder-field';
|
|
22
|
+
export { LTREE_SCALAR_NAME, LtreeCodecPlugin } from './plugins/ltree-codec';
|
|
23
|
+
export { createLtreeOperatorFactory } from './plugins/connection-filter-operators';
|
|
24
|
+
export { createFolderOperatorFactory } from './plugins/folder-filter-operators';
|
package/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* graphile-ltree — PostGraphile v5 ltree Plugin
|
|
4
|
+
*
|
|
5
|
+
* Two presets:
|
|
6
|
+
*
|
|
7
|
+
* - `GraphileLtreePreset` — base ltree operators (isAncestorOf, isDescendantOf, matchesGlob)
|
|
8
|
+
* - `GraphileFolderPreset` — folder-oriented layer (within, ancestorOf, glob) + pathFolder fields
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // For folder-style interface (recommended):
|
|
13
|
+
* import { GraphileFolderPreset } from 'graphile-ltree';
|
|
14
|
+
*
|
|
15
|
+
* // For raw ltree operators only:
|
|
16
|
+
* import { GraphileLtreePreset } from 'graphile-ltree';
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.createFolderOperatorFactory = exports.createLtreeOperatorFactory = exports.LtreeCodecPlugin = exports.LTREE_SCALAR_NAME = exports.LtreeFolderFieldPlugin = exports.LtreeExtensionDetectionPlugin = exports.GraphileLtreePreset = exports.GraphileFolderPreset = void 0;
|
|
21
|
+
// Presets
|
|
22
|
+
var preset_1 = require("./preset");
|
|
23
|
+
Object.defineProperty(exports, "GraphileFolderPreset", { enumerable: true, get: function () { return preset_1.GraphileFolderPreset; } });
|
|
24
|
+
Object.defineProperty(exports, "GraphileLtreePreset", { enumerable: true, get: function () { return preset_1.GraphileLtreePreset; } });
|
|
25
|
+
var detect_ltree_1 = require("./plugins/detect-ltree");
|
|
26
|
+
Object.defineProperty(exports, "LtreeExtensionDetectionPlugin", { enumerable: true, get: function () { return detect_ltree_1.LtreeExtensionDetectionPlugin; } });
|
|
27
|
+
var folder_field_1 = require("./plugins/folder-field");
|
|
28
|
+
Object.defineProperty(exports, "LtreeFolderFieldPlugin", { enumerable: true, get: function () { return folder_field_1.LtreeFolderFieldPlugin; } });
|
|
29
|
+
var ltree_codec_1 = require("./plugins/ltree-codec");
|
|
30
|
+
Object.defineProperty(exports, "LTREE_SCALAR_NAME", { enumerable: true, get: function () { return ltree_codec_1.LTREE_SCALAR_NAME; } });
|
|
31
|
+
Object.defineProperty(exports, "LtreeCodecPlugin", { enumerable: true, get: function () { return ltree_codec_1.LtreeCodecPlugin; } });
|
|
32
|
+
// Connection filter operator factories
|
|
33
|
+
var connection_filter_operators_1 = require("./plugins/connection-filter-operators");
|
|
34
|
+
Object.defineProperty(exports, "createLtreeOperatorFactory", { enumerable: true, get: function () { return connection_filter_operators_1.createLtreeOperatorFactory; } });
|
|
35
|
+
var folder_filter_operators_1 = require("./plugins/folder-filter-operators");
|
|
36
|
+
Object.defineProperty(exports, "createFolderOperatorFactory", { enumerable: true, get: function () { return folder_filter_operators_1.createFolderOperatorFactory; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graphile-ltree",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "PostGraphile v5 ltree plugin — auto-detects ltree columns, exposes slash-path folder fields, and provides containment/glob filter operators",
|
|
5
|
+
"author": "Constructive <developers@constructive.io>",
|
|
6
|
+
"homepage": "https://github.com/constructive-io/constructive",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"module": "esm/index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"clean": "makage clean",
|
|
13
|
+
"prepack": "npm run build",
|
|
14
|
+
"build": "makage build",
|
|
15
|
+
"build:dev": "makage build --dev",
|
|
16
|
+
"lint": "eslint . --fix",
|
|
17
|
+
"test": "jest --forceExit",
|
|
18
|
+
"test:watch": "jest --watch"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"directory": "dist"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/constructive-io/constructive"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/constructive-io/constructive/issues"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@dataplan/pg": "1.0.0",
|
|
33
|
+
"grafast": "1.0.0",
|
|
34
|
+
"graphile-build": "5.0.0",
|
|
35
|
+
"graphile-build-pg": "5.0.0",
|
|
36
|
+
"graphile-config": "1.0.0",
|
|
37
|
+
"graphile-connection-filter": "^1.5.6",
|
|
38
|
+
"graphql": "16.13.0",
|
|
39
|
+
"pg-sql2": "5.0.0",
|
|
40
|
+
"postgraphile": "5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"graphile-connection-filter": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.19.11",
|
|
49
|
+
"graphile-test": "^4.9.5",
|
|
50
|
+
"makage": "^0.3.0",
|
|
51
|
+
"pgsql-test": "^4.9.5"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"postgraphile",
|
|
55
|
+
"graphile",
|
|
56
|
+
"constructive",
|
|
57
|
+
"plugin",
|
|
58
|
+
"postgres",
|
|
59
|
+
"graphql",
|
|
60
|
+
"ltree",
|
|
61
|
+
"hierarchy",
|
|
62
|
+
"path",
|
|
63
|
+
"folder",
|
|
64
|
+
"tree"
|
|
65
|
+
],
|
|
66
|
+
"gitHead": "cbbd6c688fcda5a307461af50b06e994ab1f8ea4"
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import 'graphile-build';
|
|
2
|
+
import 'graphile-build-pg';
|
|
3
|
+
import 'graphile-connection-filter';
|
|
4
|
+
import type { ConnectionFilterOperatorFactory } from 'graphile-connection-filter';
|
|
5
|
+
/**
|
|
6
|
+
* Creates the ltree connection filter operator factory.
|
|
7
|
+
*
|
|
8
|
+
* Registers operators on the LTree scalar type (only ltree columns):
|
|
9
|
+
*
|
|
10
|
+
* - `isAncestorOf` -- column <@ to_path(value): finds descendants
|
|
11
|
+
* - `isDescendantOf` -- column @> to_path(value): finds ancestors
|
|
12
|
+
* - `matchesGlob` -- column ~ to_query(value): glob/lquery pattern matching
|
|
13
|
+
*
|
|
14
|
+
* All operators accept dot-delimited paths or slash-delimited paths.
|
|
15
|
+
* When ltree_helpers is installed, conversion happens server-side via
|
|
16
|
+
* to_path() / to_query(). Otherwise, inline SQL casts are used.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLtreeOperatorFactory(): ConnectionFilterOperatorFactory;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createLtreeOperatorFactory = createLtreeOperatorFactory;
|
|
7
|
+
require("graphile-build");
|
|
8
|
+
require("graphile-build-pg");
|
|
9
|
+
require("graphile-connection-filter");
|
|
10
|
+
const pg_sql2_1 = __importDefault(require("pg-sql2"));
|
|
11
|
+
const ltree_codec_1 = require("./ltree-codec");
|
|
12
|
+
function hasLtreeHelpers(build) {
|
|
13
|
+
const pgRegistry = build.input?.pgRegistry;
|
|
14
|
+
if (!pgRegistry)
|
|
15
|
+
return false;
|
|
16
|
+
for (const resource of Object.values(pgRegistry.pgResources)) {
|
|
17
|
+
const r = resource;
|
|
18
|
+
if (r?.extensions?.pg?.schemaName === 'ltree_helpers')
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function toPathExpr(value, useHelpers) {
|
|
24
|
+
if (useHelpers) {
|
|
25
|
+
return pg_sql2_1.default.fragment `ltree_helpers.to_path(${value})`;
|
|
26
|
+
}
|
|
27
|
+
return pg_sql2_1.default.fragment `replace(ltrim(${value}, '/'), '/', '.')::ltree`;
|
|
28
|
+
}
|
|
29
|
+
function toQueryExpr(value, useHelpers) {
|
|
30
|
+
if (useHelpers) {
|
|
31
|
+
return pg_sql2_1.default.fragment `ltree_helpers.to_query(${value})`;
|
|
32
|
+
}
|
|
33
|
+
// Glob → lquery conversion:
|
|
34
|
+
// ** → * (0+ labels in lquery)
|
|
35
|
+
// * → *{1} (exactly 1 label)
|
|
36
|
+
// We use a placeholder to avoid ** being affected by the * → *{1} step.
|
|
37
|
+
return pg_sql2_1.default.fragment `replace(replace(replace(replace(ltrim(${value}, '/'), '**', '__DSTAR__'), '*', '*{1}'), '__DSTAR__', '*'), '/', '.')::lquery`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates the ltree connection filter operator factory.
|
|
41
|
+
*
|
|
42
|
+
* Registers operators on the LTree scalar type (only ltree columns):
|
|
43
|
+
*
|
|
44
|
+
* - `isAncestorOf` -- column <@ to_path(value): finds descendants
|
|
45
|
+
* - `isDescendantOf` -- column @> to_path(value): finds ancestors
|
|
46
|
+
* - `matchesGlob` -- column ~ to_query(value): glob/lquery pattern matching
|
|
47
|
+
*
|
|
48
|
+
* All operators accept dot-delimited paths or slash-delimited paths.
|
|
49
|
+
* When ltree_helpers is installed, conversion happens server-side via
|
|
50
|
+
* to_path() / to_query(). Otherwise, inline SQL casts are used.
|
|
51
|
+
*/
|
|
52
|
+
function createLtreeOperatorFactory() {
|
|
53
|
+
return (build) => {
|
|
54
|
+
const ltreeInfo = build.pgLtreeExtensionInfo;
|
|
55
|
+
if (!ltreeInfo)
|
|
56
|
+
return [];
|
|
57
|
+
const useHelpers = hasLtreeHelpers(build);
|
|
58
|
+
const registrations = [];
|
|
59
|
+
registrations.push({
|
|
60
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
61
|
+
operatorName: 'isAncestorOf',
|
|
62
|
+
spec: {
|
|
63
|
+
description: 'Is a descendant of (or equal to) the specified path. ' +
|
|
64
|
+
'Returns rows whose ltree path is contained by the given path.',
|
|
65
|
+
resolveType: (fieldType) => fieldType,
|
|
66
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
67
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
68
|
+
const pathVal = pg_sql2_1.default.value(String(input));
|
|
69
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} <@ ${toPathExpr(pathVal, useHelpers)}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
registrations.push({
|
|
74
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
75
|
+
operatorName: 'isDescendantOf',
|
|
76
|
+
spec: {
|
|
77
|
+
description: 'Is an ancestor of (or equal to) the specified path. ' +
|
|
78
|
+
'Returns rows whose ltree path contains the given path.',
|
|
79
|
+
resolveType: (fieldType) => fieldType,
|
|
80
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
81
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
82
|
+
const pathVal = pg_sql2_1.default.value(String(input));
|
|
83
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} @> ${toPathExpr(pathVal, useHelpers)}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
registrations.push({
|
|
88
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
89
|
+
operatorName: 'matchesGlob',
|
|
90
|
+
spec: {
|
|
91
|
+
description: 'Matches the specified glob pattern (lquery). ' +
|
|
92
|
+
'Use * for single-level wildcard, ** for recursive descent.',
|
|
93
|
+
resolveType: (fieldType) => fieldType,
|
|
94
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
95
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
96
|
+
const globVal = pg_sql2_1.default.value(String(input));
|
|
97
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} ~ ${toQueryExpr(globVal, useHelpers)}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return registrations;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import 'graphile-build';
|
|
2
|
+
import 'graphile-build-pg';
|
|
3
|
+
import type { PgCodec } from '@dataplan/pg';
|
|
4
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
5
|
+
export interface LtreeExtensionInfo {
|
|
6
|
+
ltreeCodec: PgCodec;
|
|
7
|
+
lqueryCodec: PgCodec | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* LtreeExtensionDetectionPlugin
|
|
11
|
+
*
|
|
12
|
+
* Detects ltree presence in the database by searching for ltree/lquery
|
|
13
|
+
* codecs in the pgRegistry. Stores detected info on the build object
|
|
14
|
+
* for downstream plugins.
|
|
15
|
+
*
|
|
16
|
+
* Gracefully degrades if ltree is not installed.
|
|
17
|
+
*/
|
|
18
|
+
export declare const LtreeExtensionDetectionPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LtreeExtensionDetectionPlugin = void 0;
|
|
4
|
+
require("graphile-build");
|
|
5
|
+
require("graphile-build-pg");
|
|
6
|
+
function isLtreeCodec(codec) {
|
|
7
|
+
return (codec?.name === 'ltree' ||
|
|
8
|
+
codec?.extensions?.pg?.name === 'ltree');
|
|
9
|
+
}
|
|
10
|
+
function isLqueryCodec(codec) {
|
|
11
|
+
return (codec?.name === 'lquery' ||
|
|
12
|
+
codec?.extensions?.pg?.name === 'lquery');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* LtreeExtensionDetectionPlugin
|
|
16
|
+
*
|
|
17
|
+
* Detects ltree presence in the database by searching for ltree/lquery
|
|
18
|
+
* codecs in the pgRegistry. Stores detected info on the build object
|
|
19
|
+
* for downstream plugins.
|
|
20
|
+
*
|
|
21
|
+
* Gracefully degrades if ltree is not installed.
|
|
22
|
+
*/
|
|
23
|
+
exports.LtreeExtensionDetectionPlugin = {
|
|
24
|
+
name: 'LtreeExtensionDetectionPlugin',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
description: 'Detects ltree extension in the database',
|
|
27
|
+
schema: {
|
|
28
|
+
hooks: {
|
|
29
|
+
build(build) {
|
|
30
|
+
const pgRegistry = build.input?.pgRegistry;
|
|
31
|
+
if (!pgRegistry) {
|
|
32
|
+
return build;
|
|
33
|
+
}
|
|
34
|
+
let ltreeCodec = null;
|
|
35
|
+
let lqueryCodec = null;
|
|
36
|
+
for (const codec of Object.values(pgRegistry.pgCodecs)) {
|
|
37
|
+
if (isLtreeCodec(codec)) {
|
|
38
|
+
ltreeCodec = codec;
|
|
39
|
+
}
|
|
40
|
+
else if (isLqueryCodec(codec)) {
|
|
41
|
+
lqueryCodec = codec;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!ltreeCodec) {
|
|
45
|
+
return build;
|
|
46
|
+
}
|
|
47
|
+
const ltreeInfo = {
|
|
48
|
+
ltreeCodec,
|
|
49
|
+
lqueryCodec
|
|
50
|
+
};
|
|
51
|
+
return build.extend(build, { pgLtreeExtensionInfo: ltreeInfo }, 'LtreeExtensionDetectionPlugin adding ltree build state');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import 'graphile-build';
|
|
2
|
+
import 'graphile-build-pg';
|
|
3
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
4
|
+
/**
|
|
5
|
+
* LtreeFolderFieldPlugin
|
|
6
|
+
*
|
|
7
|
+
* For every ltree column on every table:
|
|
8
|
+
*
|
|
9
|
+
* 1. Renames the raw ltree field from `{column}` to `{column}Tree`
|
|
10
|
+
* via the `@name` smart tag (e.g. `path` -> `pathTree`)
|
|
11
|
+
*
|
|
12
|
+
* 2. Adds a virtual field with the original name (`path`) that returns
|
|
13
|
+
* the slash-delimited folder path.
|
|
14
|
+
* (e.g. 'projects.alpha.docs' -> '/projects/alpha/docs')
|
|
15
|
+
*
|
|
16
|
+
* This gives users the clean field name for display while preserving
|
|
17
|
+
* the raw ltree field (with all filter operators) under `{column}Tree`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LtreeFolderFieldPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LtreeFolderFieldPlugin = void 0;
|
|
4
|
+
require("graphile-build");
|
|
5
|
+
require("graphile-build-pg");
|
|
6
|
+
const graphql_1 = require("graphql");
|
|
7
|
+
/**
|
|
8
|
+
* LtreeFolderFieldPlugin
|
|
9
|
+
*
|
|
10
|
+
* For every ltree column on every table:
|
|
11
|
+
*
|
|
12
|
+
* 1. Renames the raw ltree field from `{column}` to `{column}Tree`
|
|
13
|
+
* via the `@name` smart tag (e.g. `path` -> `pathTree`)
|
|
14
|
+
*
|
|
15
|
+
* 2. Adds a virtual field with the original name (`path`) that returns
|
|
16
|
+
* the slash-delimited folder path.
|
|
17
|
+
* (e.g. 'projects.alpha.docs' -> '/projects/alpha/docs')
|
|
18
|
+
*
|
|
19
|
+
* This gives users the clean field name for display while preserving
|
|
20
|
+
* the raw ltree field (with all filter operators) under `{column}Tree`.
|
|
21
|
+
*/
|
|
22
|
+
exports.LtreeFolderFieldPlugin = {
|
|
23
|
+
name: 'LtreeFolderFieldPlugin',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
description: 'Renames ltree columns to {col}Tree and adds virtual {col} folder fields',
|
|
26
|
+
after: ['LtreeExtensionDetectionPlugin', 'LtreeCodecPlugin', 'PgAttributesPlugin'],
|
|
27
|
+
schema: {
|
|
28
|
+
hooks: {
|
|
29
|
+
/**
|
|
30
|
+
* Before fields are built, rename ltree attributes via the @name tag.
|
|
31
|
+
* This ensures PostGraphile's plan system correctly maps the renamed
|
|
32
|
+
* field to the underlying database column.
|
|
33
|
+
*/
|
|
34
|
+
init(_, build) {
|
|
35
|
+
const ltreeInfo = build.pgLtreeExtensionInfo;
|
|
36
|
+
if (!ltreeInfo)
|
|
37
|
+
return _;
|
|
38
|
+
const pgRegistry = build.input?.pgRegistry;
|
|
39
|
+
if (!pgRegistry)
|
|
40
|
+
return _;
|
|
41
|
+
for (const resource of Object.values(pgRegistry.pgResources)) {
|
|
42
|
+
const r = resource;
|
|
43
|
+
const codec = r?.codec;
|
|
44
|
+
if (!codec?.attributes)
|
|
45
|
+
continue;
|
|
46
|
+
for (const [attrName, attr] of Object.entries(codec.attributes)) {
|
|
47
|
+
const attrCodec = attr?.codec;
|
|
48
|
+
if (!attrCodec)
|
|
49
|
+
continue;
|
|
50
|
+
const isLtree = attrCodec.name === 'ltree' ||
|
|
51
|
+
attrCodec.extensions?.pg?.name === 'ltree';
|
|
52
|
+
if (!isLtree)
|
|
53
|
+
continue;
|
|
54
|
+
// Set @name tag to rename: path → path_tree
|
|
55
|
+
// PostGraphile's _attributeName inflector reads this tag
|
|
56
|
+
attr.extensions = attr.extensions || {};
|
|
57
|
+
attr.extensions.tags = attr.extensions.tags || {};
|
|
58
|
+
attr.extensions.tags.name = `${attrName}_tree`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return _;
|
|
62
|
+
},
|
|
63
|
+
GraphQLObjectType_fields(fields, build, context) {
|
|
64
|
+
const ltreeInfo = build.pgLtreeExtensionInfo;
|
|
65
|
+
if (!ltreeInfo)
|
|
66
|
+
return fields;
|
|
67
|
+
const { scope: { isPgClassType, pgCodec: rawPgCodec }, fieldWithHooks } = context;
|
|
68
|
+
if (!isPgClassType || !rawPgCodec?.attributes)
|
|
69
|
+
return fields;
|
|
70
|
+
const codec = rawPgCodec;
|
|
71
|
+
const { lambda } = build.grafast;
|
|
72
|
+
let newFields = fields;
|
|
73
|
+
for (const [attrName, attr] of Object.entries(codec.attributes)) {
|
|
74
|
+
const attrCodec = attr?.codec;
|
|
75
|
+
if (!attrCodec)
|
|
76
|
+
continue;
|
|
77
|
+
const isLtree = attrCodec.name === 'ltree' ||
|
|
78
|
+
attrCodec.extensions?.pg?.name === 'ltree';
|
|
79
|
+
if (!isLtree)
|
|
80
|
+
continue;
|
|
81
|
+
// The original column name becomes the folder field name
|
|
82
|
+
const folderFieldName = build.inflection.camelCase(attrName);
|
|
83
|
+
newFields = build.extend(newFields, {
|
|
84
|
+
[folderFieldName]: fieldWithHooks({ fieldName: folderFieldName }, () => ({
|
|
85
|
+
description: `Slash-delimited path derived from the \`${attrName}\` ltree column. Example: \`/projects/alpha/docs\``,
|
|
86
|
+
type: graphql_1.GraphQLString,
|
|
87
|
+
plan($row) {
|
|
88
|
+
const $ltreeVal = $row.get(attrName);
|
|
89
|
+
return lambda($ltreeVal, (val) => {
|
|
90
|
+
if (val == null)
|
|
91
|
+
return null;
|
|
92
|
+
const str = String(val);
|
|
93
|
+
if (str === '')
|
|
94
|
+
return '/';
|
|
95
|
+
return '/' + str.replace(/\./g, '/');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}))
|
|
99
|
+
}, `LtreeFolderFieldPlugin adding folder field '${folderFieldName}' for '${attrName}' on '${codec.name}'`);
|
|
100
|
+
}
|
|
101
|
+
return newFields;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|