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
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;
|