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.
@@ -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
+ };
@@ -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
+ };