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 ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # graphile-ltree
2
+
3
+ PostGraphile v5 plugin for PostgreSQL's ltree hierarchical data type.
4
+
5
+ Auto-detects ltree columns, exposes slash-path folder fields, and provides containment/glob filter operators for hierarchical data.
6
+
7
+ ## Features
8
+
9
+ - **Ltree scalar type** — ltree columns are exposed as the `Ltree` GraphQL scalar instead of `String`
10
+ - **Folder fields** — virtual `{column}Folder` fields that convert dot-notation to slash paths (`projects.alpha.docs` -> `/projects/alpha/docs`)
11
+ - **Connection filter operators** — `isAncestorOf`, `isDescendantOf`, `matchesGlob` for containment and pattern queries
12
+ - **Auto-detection** — no configuration needed; the plugin scans the database for ltree columns
13
+ - **ltree_helpers integration** — automatically uses `ltree_helpers.to_path()` / `to_query()` when available
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { GraphileLtreePreset } from 'graphile-ltree';
19
+
20
+ const preset = {
21
+ extends: [GraphileLtreePreset],
22
+ };
23
+ ```
24
+
25
+ Or add to the Constructive preset (already included):
26
+
27
+ ```typescript
28
+ import { ConstructivePreset } from 'graphile-settings/presets';
29
+ ```
30
+
31
+ ## GraphQL API
32
+
33
+ ### Folder fields
34
+
35
+ For a table with an ltree column `path`:
36
+
37
+ ```graphql
38
+ {
39
+ allFiles {
40
+ nodes {
41
+ path # "projects.alpha.docs" (raw ltree)
42
+ pathFolder # "/projects/alpha/docs" (slash-delimited)
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Filter operators
49
+
50
+ ```graphql
51
+ # Files under /projects/alpha (containment)
52
+ {
53
+ allFiles(where: { path: { isAncestorOf: "projects.alpha" } }) {
54
+ nodes { filename pathFolder }
55
+ }
56
+ }
57
+
58
+ # Ancestors of a deep path
59
+ {
60
+ allFiles(where: { path: { isDescendantOf: "projects.alpha.docs.images" } }) {
61
+ nodes { filename pathFolder }
62
+ }
63
+ }
64
+
65
+ # Glob/lquery pattern matching
66
+ {
67
+ allFiles(where: { path: { matchesGlob: "projects.*.docs" } }) {
68
+ nodes { filename pathFolder }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Plugins
74
+
75
+ | Plugin | Purpose |
76
+ |--------|---------|
77
+ | `LtreeExtensionDetectionPlugin` | Scans pgRegistry for ltree/lquery codecs |
78
+ | `LtreeCodecPlugin` | Registers `Ltree` scalar, maps ltree/lquery types |
79
+ | `LtreeFolderFieldPlugin` | Adds virtual `{column}Folder` fields |
80
+ | `createLtreeOperatorFactory()` | Connection filter operators for containment/glob |
81
+
82
+ ---
83
+
84
+ ## Education and Tutorials
85
+
86
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
87
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
88
+
89
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
90
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
91
+
92
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
93
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
94
+
95
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
96
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
97
+
98
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
99
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
100
+
101
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
102
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
103
+
104
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
105
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
106
+
107
+ ## Related Constructive Tooling
108
+
109
+ ### 📦 Package Management
110
+
111
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
112
+
113
+ ### 🧪 Testing
114
+
115
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
116
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
117
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
118
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
119
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
120
+
121
+ ### 🧠 Parsing & AST
122
+
123
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
124
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
125
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
126
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
127
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
128
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
129
+
130
+ ## Credits
131
+
132
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
133
+
134
+ ## Disclaimer
135
+
136
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
137
+
138
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
package/esm/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/esm/index.js ADDED
@@ -0,0 +1,25 @@
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
+ // Presets
19
+ export { GraphileFolderPreset, GraphileLtreePreset } from './preset';
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
+ // Connection filter operator factories
24
+ export { createLtreeOperatorFactory } from './plugins/connection-filter-operators';
25
+ export { createFolderOperatorFactory } from './plugins/folder-filter-operators';
@@ -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,97 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import 'graphile-connection-filter';
4
+ import sql from 'pg-sql2';
5
+ import { LTREE_SCALAR_NAME } from './ltree-codec';
6
+ function hasLtreeHelpers(build) {
7
+ const pgRegistry = build.input?.pgRegistry;
8
+ if (!pgRegistry)
9
+ return false;
10
+ for (const resource of Object.values(pgRegistry.pgResources)) {
11
+ const r = resource;
12
+ if (r?.extensions?.pg?.schemaName === 'ltree_helpers')
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ function toPathExpr(value, useHelpers) {
18
+ if (useHelpers) {
19
+ return sql.fragment `ltree_helpers.to_path(${value})`;
20
+ }
21
+ return sql.fragment `replace(ltrim(${value}, '/'), '/', '.')::ltree`;
22
+ }
23
+ function toQueryExpr(value, useHelpers) {
24
+ if (useHelpers) {
25
+ return sql.fragment `ltree_helpers.to_query(${value})`;
26
+ }
27
+ // Glob → lquery conversion:
28
+ // ** → * (0+ labels in lquery)
29
+ // * → *{1} (exactly 1 label)
30
+ // We use a placeholder to avoid ** being affected by the * → *{1} step.
31
+ return sql.fragment `replace(replace(replace(replace(ltrim(${value}, '/'), '**', '__DSTAR__'), '*', '*{1}'), '__DSTAR__', '*'), '/', '.')::lquery`;
32
+ }
33
+ /**
34
+ * Creates the ltree connection filter operator factory.
35
+ *
36
+ * Registers operators on the LTree scalar type (only ltree columns):
37
+ *
38
+ * - `isAncestorOf` -- column <@ to_path(value): finds descendants
39
+ * - `isDescendantOf` -- column @> to_path(value): finds ancestors
40
+ * - `matchesGlob` -- column ~ to_query(value): glob/lquery pattern matching
41
+ *
42
+ * All operators accept dot-delimited paths or slash-delimited paths.
43
+ * When ltree_helpers is installed, conversion happens server-side via
44
+ * to_path() / to_query(). Otherwise, inline SQL casts are used.
45
+ */
46
+ export function createLtreeOperatorFactory() {
47
+ return (build) => {
48
+ const ltreeInfo = build.pgLtreeExtensionInfo;
49
+ if (!ltreeInfo)
50
+ return [];
51
+ const useHelpers = hasLtreeHelpers(build);
52
+ const registrations = [];
53
+ registrations.push({
54
+ typeNames: LTREE_SCALAR_NAME,
55
+ operatorName: 'isAncestorOf',
56
+ spec: {
57
+ description: 'Is a descendant of (or equal to) the specified path. ' +
58
+ 'Returns rows whose ltree path is contained by the given path.',
59
+ resolveType: (fieldType) => fieldType,
60
+ resolveSqlValue: () => sql.null,
61
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
62
+ const pathVal = sql.value(String(input));
63
+ return sql.fragment `${sqlIdentifier} <@ ${toPathExpr(pathVal, useHelpers)}`;
64
+ }
65
+ }
66
+ });
67
+ registrations.push({
68
+ typeNames: LTREE_SCALAR_NAME,
69
+ operatorName: 'isDescendantOf',
70
+ spec: {
71
+ description: 'Is an ancestor of (or equal to) the specified path. ' +
72
+ 'Returns rows whose ltree path contains the given path.',
73
+ resolveType: (fieldType) => fieldType,
74
+ resolveSqlValue: () => sql.null,
75
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
76
+ const pathVal = sql.value(String(input));
77
+ return sql.fragment `${sqlIdentifier} @> ${toPathExpr(pathVal, useHelpers)}`;
78
+ }
79
+ }
80
+ });
81
+ registrations.push({
82
+ typeNames: LTREE_SCALAR_NAME,
83
+ operatorName: 'matchesGlob',
84
+ spec: {
85
+ description: 'Matches the specified glob pattern (lquery). ' +
86
+ 'Use * for single-level wildcard, ** for recursive descent.',
87
+ resolveType: (fieldType) => fieldType,
88
+ resolveSqlValue: () => sql.null,
89
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
90
+ const globVal = sql.value(String(input));
91
+ return sql.fragment `${sqlIdentifier} ~ ${toQueryExpr(globVal, useHelpers)}`;
92
+ }
93
+ }
94
+ });
95
+ return registrations;
96
+ };
97
+ }
@@ -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,52 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ function isLtreeCodec(codec) {
4
+ return (codec?.name === 'ltree' ||
5
+ codec?.extensions?.pg?.name === 'ltree');
6
+ }
7
+ function isLqueryCodec(codec) {
8
+ return (codec?.name === 'lquery' ||
9
+ codec?.extensions?.pg?.name === 'lquery');
10
+ }
11
+ /**
12
+ * LtreeExtensionDetectionPlugin
13
+ *
14
+ * Detects ltree presence in the database by searching for ltree/lquery
15
+ * codecs in the pgRegistry. Stores detected info on the build object
16
+ * for downstream plugins.
17
+ *
18
+ * Gracefully degrades if ltree is not installed.
19
+ */
20
+ export const LtreeExtensionDetectionPlugin = {
21
+ name: 'LtreeExtensionDetectionPlugin',
22
+ version: '1.0.0',
23
+ description: 'Detects ltree extension in the database',
24
+ schema: {
25
+ hooks: {
26
+ build(build) {
27
+ const pgRegistry = build.input?.pgRegistry;
28
+ if (!pgRegistry) {
29
+ return build;
30
+ }
31
+ let ltreeCodec = null;
32
+ let lqueryCodec = null;
33
+ for (const codec of Object.values(pgRegistry.pgCodecs)) {
34
+ if (isLtreeCodec(codec)) {
35
+ ltreeCodec = codec;
36
+ }
37
+ else if (isLqueryCodec(codec)) {
38
+ lqueryCodec = codec;
39
+ }
40
+ }
41
+ if (!ltreeCodec) {
42
+ return build;
43
+ }
44
+ const ltreeInfo = {
45
+ ltreeCodec,
46
+ lqueryCodec
47
+ };
48
+ return build.extend(build, { pgLtreeExtensionInfo: ltreeInfo }, 'LtreeExtensionDetectionPlugin adding ltree build state');
49
+ }
50
+ }
51
+ }
52
+ };
@@ -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,102 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import { GraphQLString } from 'graphql';
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 const LtreeFolderFieldPlugin = {
20
+ name: 'LtreeFolderFieldPlugin',
21
+ version: '1.0.0',
22
+ description: 'Renames ltree columns to {col}Tree and adds virtual {col} folder fields',
23
+ after: ['LtreeExtensionDetectionPlugin', 'LtreeCodecPlugin', 'PgAttributesPlugin'],
24
+ schema: {
25
+ hooks: {
26
+ /**
27
+ * Before fields are built, rename ltree attributes via the @name tag.
28
+ * This ensures PostGraphile's plan system correctly maps the renamed
29
+ * field to the underlying database column.
30
+ */
31
+ init(_, build) {
32
+ const ltreeInfo = build.pgLtreeExtensionInfo;
33
+ if (!ltreeInfo)
34
+ return _;
35
+ const pgRegistry = build.input?.pgRegistry;
36
+ if (!pgRegistry)
37
+ return _;
38
+ for (const resource of Object.values(pgRegistry.pgResources)) {
39
+ const r = resource;
40
+ const codec = r?.codec;
41
+ if (!codec?.attributes)
42
+ continue;
43
+ for (const [attrName, attr] of Object.entries(codec.attributes)) {
44
+ const attrCodec = attr?.codec;
45
+ if (!attrCodec)
46
+ continue;
47
+ const isLtree = attrCodec.name === 'ltree' ||
48
+ attrCodec.extensions?.pg?.name === 'ltree';
49
+ if (!isLtree)
50
+ continue;
51
+ // Set @name tag to rename: path → path_tree
52
+ // PostGraphile's _attributeName inflector reads this tag
53
+ attr.extensions = attr.extensions || {};
54
+ attr.extensions.tags = attr.extensions.tags || {};
55
+ attr.extensions.tags.name = `${attrName}_tree`;
56
+ }
57
+ }
58
+ return _;
59
+ },
60
+ GraphQLObjectType_fields(fields, build, context) {
61
+ const ltreeInfo = build.pgLtreeExtensionInfo;
62
+ if (!ltreeInfo)
63
+ return fields;
64
+ const { scope: { isPgClassType, pgCodec: rawPgCodec }, fieldWithHooks } = context;
65
+ if (!isPgClassType || !rawPgCodec?.attributes)
66
+ return fields;
67
+ const codec = rawPgCodec;
68
+ const { lambda } = build.grafast;
69
+ let newFields = fields;
70
+ for (const [attrName, attr] of Object.entries(codec.attributes)) {
71
+ const attrCodec = attr?.codec;
72
+ if (!attrCodec)
73
+ continue;
74
+ const isLtree = attrCodec.name === 'ltree' ||
75
+ attrCodec.extensions?.pg?.name === 'ltree';
76
+ if (!isLtree)
77
+ continue;
78
+ // The original column name becomes the folder field name
79
+ const folderFieldName = build.inflection.camelCase(attrName);
80
+ newFields = build.extend(newFields, {
81
+ [folderFieldName]: fieldWithHooks({ fieldName: folderFieldName }, () => ({
82
+ description: `Slash-delimited path derived from the \`${attrName}\` ltree column. Example: \`/projects/alpha/docs\``,
83
+ type: GraphQLString,
84
+ plan($row) {
85
+ const $ltreeVal = $row.get(attrName);
86
+ return lambda($ltreeVal, (val) => {
87
+ if (val == null)
88
+ return null;
89
+ const str = String(val);
90
+ if (str === '')
91
+ return '/';
92
+ return '/' + str.replace(/\./g, '/');
93
+ });
94
+ }
95
+ }))
96
+ }, `LtreeFolderFieldPlugin adding folder field '${folderFieldName}' for '${attrName}' on '${codec.name}'`);
97
+ }
98
+ return newFields;
99
+ }
100
+ }
101
+ }
102
+ };
@@ -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,99 @@
1
+ import 'graphile-build';
2
+ import 'graphile-build-pg';
3
+ import 'graphile-connection-filter';
4
+ import sql from 'pg-sql2';
5
+ import { LTREE_SCALAR_NAME } from './ltree-codec';
6
+ function hasLtreeHelpers(build) {
7
+ const pgRegistry = build.input?.pgRegistry;
8
+ if (!pgRegistry)
9
+ return false;
10
+ for (const resource of Object.values(pgRegistry.pgResources)) {
11
+ const r = resource;
12
+ if (r?.extensions?.pg?.schemaName === 'ltree_helpers')
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ function slashToLtree(value, useHelpers) {
18
+ if (useHelpers) {
19
+ return sql.fragment `ltree_helpers.to_path(${value})`;
20
+ }
21
+ return sql.fragment `replace(ltrim(${value}, '/'), '/', '.')::ltree`;
22
+ }
23
+ function slashGlobToLquery(value, useHelpers) {
24
+ if (useHelpers) {
25
+ return sql.fragment `ltree_helpers.to_query(${value})`;
26
+ }
27
+ return sql.fragment `replace(replace(replace(replace(ltrim(${value}, '/'), '**', '__DSTAR__'), '*', '*{1}'), '__DSTAR__', '*'), '/', '.')::lquery`;
28
+ }
29
+ /**
30
+ * Creates folder-oriented connection filter operators for the LTree scalar.
31
+ *
32
+ * These are user-friendly aliases for the raw ltree operators, designed for
33
+ * slash-delimited file/folder paths:
34
+ *
35
+ * - `within` — files within a folder: path <@ to_path('/projects/alpha')
36
+ * - `ancestorOf` — ancestor folders: path @> to_path('/projects/alpha/docs')
37
+ * - `glob` — glob pattern: path ~ to_query('/projects/* /docs')
38
+ *
39
+ * All inputs use slash-delimited paths. Conversion to ltree happens server-side.
40
+ *
41
+ * @example
42
+ * ```graphql
43
+ * allFiles(where: { path: { within: "/projects/alpha" } }) { ... }
44
+ * allFiles(where: { path: { glob: "/projects/* /docs" } }) { ... }
45
+ * ```
46
+ */
47
+ export function createFolderOperatorFactory() {
48
+ return (build) => {
49
+ const ltreeInfo = build.pgLtreeExtensionInfo;
50
+ if (!ltreeInfo)
51
+ return [];
52
+ const useHelpers = hasLtreeHelpers(build);
53
+ const registrations = [];
54
+ registrations.push({
55
+ typeNames: LTREE_SCALAR_NAME,
56
+ operatorName: 'within',
57
+ spec: {
58
+ description: 'Files within the specified folder (inclusive). ' +
59
+ 'Accepts slash-delimited paths like "/projects/alpha".',
60
+ resolveType: (fieldType) => fieldType,
61
+ resolveSqlValue: () => sql.null,
62
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
63
+ const pathVal = sql.value(String(input));
64
+ return sql.fragment `${sqlIdentifier} <@ ${slashToLtree(pathVal, useHelpers)}`;
65
+ }
66
+ }
67
+ });
68
+ registrations.push({
69
+ typeNames: LTREE_SCALAR_NAME,
70
+ operatorName: 'ancestorOf',
71
+ spec: {
72
+ description: 'Ancestor folders of the specified path. ' +
73
+ 'Accepts slash-delimited paths like "/projects/alpha/docs".',
74
+ resolveType: (fieldType) => fieldType,
75
+ resolveSqlValue: () => sql.null,
76
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
77
+ const pathVal = sql.value(String(input));
78
+ return sql.fragment `${sqlIdentifier} @> ${slashToLtree(pathVal, useHelpers)}`;
79
+ }
80
+ }
81
+ });
82
+ registrations.push({
83
+ typeNames: LTREE_SCALAR_NAME,
84
+ operatorName: 'glob',
85
+ spec: {
86
+ description: 'Matches a slash-delimited glob pattern. ' +
87
+ 'Use * for single-level wildcard, ** for recursive descent. ' +
88
+ 'Example: "/projects/*/docs".',
89
+ resolveType: (fieldType) => fieldType,
90
+ resolveSqlValue: () => sql.null,
91
+ resolve(sqlIdentifier, _sqlValue, input, _$where, _details) {
92
+ const globVal = sql.value(String(input));
93
+ return sql.fragment `${sqlIdentifier} ~ ${slashGlobToLquery(globVal, useHelpers)}`;
94
+ }
95
+ }
96
+ });
97
+ return registrations;
98
+ };
99
+ }
@@ -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;