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,23 @@
|
|
|
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 folder-oriented connection filter operators for the LTree scalar.
|
|
7
|
+
*
|
|
8
|
+
* These are user-friendly aliases for the raw ltree operators, designed for
|
|
9
|
+
* slash-delimited file/folder paths:
|
|
10
|
+
*
|
|
11
|
+
* - `within` — files within a folder: path <@ to_path('/projects/alpha')
|
|
12
|
+
* - `ancestorOf` — ancestor folders: path @> to_path('/projects/alpha/docs')
|
|
13
|
+
* - `glob` — glob pattern: path ~ to_query('/projects/* /docs')
|
|
14
|
+
*
|
|
15
|
+
* All inputs use slash-delimited paths. Conversion to ltree happens server-side.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```graphql
|
|
19
|
+
* allFiles(where: { path: { within: "/projects/alpha" } }) { ... }
|
|
20
|
+
* allFiles(where: { path: { glob: "/projects/* /docs" } }) { ... }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function createFolderOperatorFactory(): ConnectionFilterOperatorFactory;
|
|
@@ -0,0 +1,105 @@
|
|
|
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.createFolderOperatorFactory = createFolderOperatorFactory;
|
|
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 slashToLtree(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 slashGlobToLquery(value, useHelpers) {
|
|
30
|
+
if (useHelpers) {
|
|
31
|
+
return pg_sql2_1.default.fragment `ltree_helpers.to_query(${value})`;
|
|
32
|
+
}
|
|
33
|
+
return pg_sql2_1.default.fragment `replace(replace(replace(replace(ltrim(${value}, '/'), '**', '__DSTAR__'), '*', '*{1}'), '__DSTAR__', '*'), '/', '.')::lquery`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates folder-oriented connection filter operators for the LTree scalar.
|
|
37
|
+
*
|
|
38
|
+
* These are user-friendly aliases for the raw ltree operators, designed for
|
|
39
|
+
* slash-delimited file/folder paths:
|
|
40
|
+
*
|
|
41
|
+
* - `within` — files within a folder: path <@ to_path('/projects/alpha')
|
|
42
|
+
* - `ancestorOf` — ancestor folders: path @> to_path('/projects/alpha/docs')
|
|
43
|
+
* - `glob` — glob pattern: path ~ to_query('/projects/* /docs')
|
|
44
|
+
*
|
|
45
|
+
* All inputs use slash-delimited paths. Conversion to ltree happens server-side.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```graphql
|
|
49
|
+
* allFiles(where: { path: { within: "/projects/alpha" } }) { ... }
|
|
50
|
+
* allFiles(where: { path: { glob: "/projects/* /docs" } }) { ... }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
function createFolderOperatorFactory() {
|
|
54
|
+
return (build) => {
|
|
55
|
+
const ltreeInfo = build.pgLtreeExtensionInfo;
|
|
56
|
+
if (!ltreeInfo)
|
|
57
|
+
return [];
|
|
58
|
+
const useHelpers = hasLtreeHelpers(build);
|
|
59
|
+
const registrations = [];
|
|
60
|
+
registrations.push({
|
|
61
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
62
|
+
operatorName: 'within',
|
|
63
|
+
spec: {
|
|
64
|
+
description: 'Files within the specified folder (inclusive). ' +
|
|
65
|
+
'Accepts slash-delimited paths like "/projects/alpha".',
|
|
66
|
+
resolveType: (fieldType) => fieldType,
|
|
67
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
68
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
69
|
+
const pathVal = pg_sql2_1.default.value(String(input));
|
|
70
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} <@ ${slashToLtree(pathVal, useHelpers)}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
registrations.push({
|
|
75
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
76
|
+
operatorName: 'ancestorOf',
|
|
77
|
+
spec: {
|
|
78
|
+
description: 'Ancestor folders of the specified path. ' +
|
|
79
|
+
'Accepts slash-delimited paths like "/projects/alpha/docs".',
|
|
80
|
+
resolveType: (fieldType) => fieldType,
|
|
81
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
82
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
83
|
+
const pathVal = pg_sql2_1.default.value(String(input));
|
|
84
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} @> ${slashToLtree(pathVal, useHelpers)}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
registrations.push({
|
|
89
|
+
typeNames: ltree_codec_1.LTREE_SCALAR_NAME,
|
|
90
|
+
operatorName: 'glob',
|
|
91
|
+
spec: {
|
|
92
|
+
description: 'Matches a slash-delimited glob pattern. ' +
|
|
93
|
+
'Use * for single-level wildcard, ** for recursive descent. ' +
|
|
94
|
+
'Example: "/projects/*/docs".',
|
|
95
|
+
resolveType: (fieldType) => fieldType,
|
|
96
|
+
resolveSqlValue: () => pg_sql2_1.default.null,
|
|
97
|
+
resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
|
|
98
|
+
const globVal = pg_sql2_1.default.value(String(input));
|
|
99
|
+
return pg_sql2_1.default.fragment `${sqlIdentifier} ~ ${slashGlobToLquery(globVal, useHelpers)}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return registrations;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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 type { GraphileConfig } from 'graphile-config';
|
|
15
|
+
export declare const LTREE_SCALAR_NAME = "LTree";
|
|
16
|
+
export declare const LtreeCodecPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LtreeCodecPlugin
|
|
4
|
+
*
|
|
5
|
+
* Ensures PostGraphile v5 properly handles ltree, lquery, and ltxtquery types.
|
|
6
|
+
*
|
|
7
|
+
* PostGraphile v5 (rc.8+) natively maps ltree to a GraphQL scalar named "LTree".
|
|
8
|
+
* This plugin:
|
|
9
|
+
* 1. Falls back to registering codecs if PostGraphile's native handling misses them
|
|
10
|
+
* 2. Re-enables ltree columns for select/filterBy (overrides rc.8 HIDE_BY_DEFAULT)
|
|
11
|
+
*
|
|
12
|
+
* The native scalar name is "LTree" (capital T). All operators and downstream
|
|
13
|
+
* plugins should reference LTREE_SCALAR_NAME for consistency.
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.LtreeCodecPlugin = exports.LTREE_SCALAR_NAME = void 0;
|
|
20
|
+
const graphql_1 = require("graphql");
|
|
21
|
+
const pg_sql2_1 = __importDefault(require("pg-sql2"));
|
|
22
|
+
exports.LTREE_SCALAR_NAME = 'LTree';
|
|
23
|
+
exports.LtreeCodecPlugin = {
|
|
24
|
+
name: 'LtreeCodecPlugin',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
description: 'Ensures ltree/lquery codecs are properly registered and enabled',
|
|
27
|
+
gather: {
|
|
28
|
+
hooks: {
|
|
29
|
+
async pgCodecs_findPgCodec(info, event) {
|
|
30
|
+
if (event.pgCodec)
|
|
31
|
+
return;
|
|
32
|
+
const { pgType: type, serviceName } = event;
|
|
33
|
+
const isLtree = type.typname === 'ltree';
|
|
34
|
+
const isLquery = type.typname === 'lquery';
|
|
35
|
+
const isLtxtquery = type.typname === 'ltxtquery';
|
|
36
|
+
if (!isLtree && !isLquery && !isLtxtquery)
|
|
37
|
+
return;
|
|
38
|
+
const ns = await info.helpers.pgIntrospection.getNamespace(serviceName, type.typnamespace);
|
|
39
|
+
const schemaName = ns?.nspname || 'pg_catalog';
|
|
40
|
+
event.pgCodec = {
|
|
41
|
+
name: type.typname,
|
|
42
|
+
sqlType: pg_sql2_1.default.identifier(schemaName, type.typname),
|
|
43
|
+
fromPg: (value) => value,
|
|
44
|
+
toPg: (value) => value,
|
|
45
|
+
attributes: undefined,
|
|
46
|
+
executor: null,
|
|
47
|
+
extensions: {
|
|
48
|
+
oid: type._id,
|
|
49
|
+
pg: {
|
|
50
|
+
serviceName,
|
|
51
|
+
schemaName,
|
|
52
|
+
name: type.typname
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
schema: {
|
|
60
|
+
hooks: {
|
|
61
|
+
init: {
|
|
62
|
+
before: ['PgCodecs', 'PgConnectionArgFilterPlugin'],
|
|
63
|
+
callback(_, build) {
|
|
64
|
+
const { setGraphQLTypeForPgCodec, hasGraphQLTypeForPgCodec } = build;
|
|
65
|
+
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
|
|
66
|
+
if (codec.name === 'ltree') {
|
|
67
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'input')) {
|
|
68
|
+
build.registerScalarType(exports.LTREE_SCALAR_NAME, {}, () => ({
|
|
69
|
+
description: 'A PostgreSQL ltree hierarchical label path, represented as a dot-delimited string (e.g. "projects.alpha.docs").',
|
|
70
|
+
serialize(value) {
|
|
71
|
+
return String(value);
|
|
72
|
+
},
|
|
73
|
+
parseValue(value) {
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
return value;
|
|
76
|
+
throw new Error(`${exports.LTREE_SCALAR_NAME} must be a string`);
|
|
77
|
+
},
|
|
78
|
+
parseLiteral(lit) {
|
|
79
|
+
if (lit.kind === 'NullValue')
|
|
80
|
+
return null;
|
|
81
|
+
if (lit.kind !== 'StringValue') {
|
|
82
|
+
throw new Error(`${exports.LTREE_SCALAR_NAME} must be a string`);
|
|
83
|
+
}
|
|
84
|
+
return lit.value;
|
|
85
|
+
}
|
|
86
|
+
}), `LtreeCodecPlugin registering ${exports.LTREE_SCALAR_NAME} scalar`);
|
|
87
|
+
setGraphQLTypeForPgCodec(codec, 'input', exports.LTREE_SCALAR_NAME);
|
|
88
|
+
setGraphQLTypeForPgCodec(codec, 'output', exports.LTREE_SCALAR_NAME);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (codec.name === 'lquery' || codec.name === 'ltxtquery') {
|
|
92
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'input')) {
|
|
93
|
+
setGraphQLTypeForPgCodec(codec, 'input', graphql_1.GraphQLString.name);
|
|
94
|
+
}
|
|
95
|
+
if (!hasGraphQLTypeForPgCodec(codec, 'output')) {
|
|
96
|
+
setGraphQLTypeForPgCodec(codec, 'output', graphql_1.GraphQLString.name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return _;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
entityBehavior: {
|
|
105
|
+
pgCodecAttribute: {
|
|
106
|
+
inferred: {
|
|
107
|
+
after: ['default'],
|
|
108
|
+
provides: ['ltreeCodecPlugin'],
|
|
109
|
+
callback(behavior, [codec, attributeName]) {
|
|
110
|
+
const attr = codec.attributes?.[attributeName];
|
|
111
|
+
const attrCodec = attr?.codec;
|
|
112
|
+
const isLtree = attrCodec?.name === 'ltree' ||
|
|
113
|
+
attrCodec?.extensions?.pg?.name === 'ltree';
|
|
114
|
+
if (!isLtree)
|
|
115
|
+
return behavior;
|
|
116
|
+
return [
|
|
117
|
+
behavior,
|
|
118
|
+
'attribute:select',
|
|
119
|
+
'attribute:base',
|
|
120
|
+
'condition:attribute:filterBy'
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
package/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/preset.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphileFolderPreset = exports.GraphileLtreePreset = void 0;
|
|
4
|
+
const connection_filter_operators_1 = require("./plugins/connection-filter-operators");
|
|
5
|
+
const detect_ltree_1 = require("./plugins/detect-ltree");
|
|
6
|
+
const folder_field_1 = require("./plugins/folder-field");
|
|
7
|
+
const folder_filter_operators_1 = require("./plugins/folder-filter-operators");
|
|
8
|
+
const ltree_codec_1 = require("./plugins/ltree-codec");
|
|
9
|
+
/**
|
|
10
|
+
* GraphileLtreePreset
|
|
11
|
+
*
|
|
12
|
+
* Base preset: ltree codec, detection, and raw ltree operators.
|
|
13
|
+
*
|
|
14
|
+
* Operators on LTree fields: isAncestorOf, isDescendantOf, matchesGlob
|
|
15
|
+
* (accept dot-delimited or slash-delimited paths)
|
|
16
|
+
*/
|
|
17
|
+
exports.GraphileLtreePreset = {
|
|
18
|
+
plugins: [
|
|
19
|
+
detect_ltree_1.LtreeExtensionDetectionPlugin,
|
|
20
|
+
ltree_codec_1.LtreeCodecPlugin
|
|
21
|
+
],
|
|
22
|
+
schema: {
|
|
23
|
+
connectionFilterOperatorFactories: [
|
|
24
|
+
(0, connection_filter_operators_1.createLtreeOperatorFactory)()
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* GraphileFolderPreset
|
|
30
|
+
*
|
|
31
|
+
* Folder-oriented layer on top of the base ltree preset.
|
|
32
|
+
*
|
|
33
|
+
* Adds:
|
|
34
|
+
* - Virtual `{column}Folder` fields with slash-delimited paths
|
|
35
|
+
* - Folder operators: within, ancestorOf, glob (always slash-delimited)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```graphql
|
|
39
|
+
* allFiles(where: { path: { within: "/projects/alpha" } }) {
|
|
40
|
+
* nodes { pathFolder filename }
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
exports.GraphileFolderPreset = {
|
|
45
|
+
extends: [exports.GraphileLtreePreset],
|
|
46
|
+
plugins: [
|
|
47
|
+
folder_field_1.LtreeFolderFieldPlugin
|
|
48
|
+
],
|
|
49
|
+
schema: {
|
|
50
|
+
connectionFilterOperatorFactories: [
|
|
51
|
+
(0, folder_filter_operators_1.createFolderOperatorFactory)()
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
exports.default = exports.GraphileFolderPreset;
|