Skip to content

Commit 5b76fca

Browse files
committed
merge: resolve conflicts with main (ltree rename)
2 parents 88bbe1a + a5ae7a6 commit 5b76fca

21 files changed

Lines changed: 1694 additions & 1939 deletions

File tree

graphile/graphile-ltree/src/__tests__/ltree.test.ts

Lines changed: 40 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@ import { getConnections, seed } from 'graphile-test';
44
import { join } from 'path';
55
import type { PgTestClient } from 'pgsql-test';
66

7-
import { createLtreeOperatorFactory } from '../plugins/connection-filter-operators';
8-
import { LtreeExtensionDetectionPlugin } from '../plugins/detect-ltree';
9-
import { LtreeFolderFieldPlugin } from '../plugins/folder-field';
107
import { createFolderOperatorFactory } from '../plugins/folder-filter-operators';
8+
import { LtreeExtensionDetectionPlugin } from '../plugins/detect-ltree';
119
import { LtreeCodecPlugin } from '../plugins/ltree-codec';
1210

1311
interface FileNode {
1412
id: number;
1513
filename: string;
1614
path: string;
17-
pathTree: string;
1815
}
1916

2017
interface AllFilesResult {
@@ -27,7 +24,6 @@ interface CategoryNode {
2724
id: number;
2825
name: string;
2926
treePath: string;
30-
treePathTree: string;
3127
}
3228

3329
interface AllCategoriesResult {
@@ -53,12 +49,10 @@ describe('graphile-ltree', () => {
5349
],
5450
plugins: [
5551
LtreeExtensionDetectionPlugin,
56-
LtreeCodecPlugin,
57-
LtreeFolderFieldPlugin
52+
LtreeCodecPlugin
5853
],
5954
schema: {
6055
connectionFilterOperatorFactories: [
61-
createLtreeOperatorFactory(),
6256
createFolderOperatorFactory()
6357
]
6458
}
@@ -101,35 +95,20 @@ describe('graphile-ltree', () => {
10195
await db.afterEach();
10296
});
10397

104-
// ─── Field naming ─────────────────────────────────────────────────────
98+
// ─── File-path output ──────────────────────────────────────────────────
10599

106-
describe('field naming', () => {
107-
it('renames ltree column to pathTree (dot-delimited)', async () => {
100+
describe('file-path output', () => {
101+
it('returns path as slash-delimited file path', async () => {
108102
const result = await query<AllFilesResult>(`{
109103
allFiles {
110-
nodes { id filename pathTree }
104+
nodes { id filename path }
111105
}
112106
}`);
113107
expect(result.errors).toBeUndefined();
114108
const nodes = result.data!.allFiles.nodes;
115109
expect(nodes.length).toBe(10);
116110
const docsFile = nodes.find(n => n.filename === 'contract.pdf');
117111
expect(docsFile).toBeDefined();
118-
expect(docsFile!.pathTree).toBe('projects.alpha.docs');
119-
});
120-
121-
it('exposes path as slash-delimited folder field', async () => {
122-
const result = await query<AllFilesResult>(`{
123-
allFiles {
124-
nodes { filename path pathTree }
125-
}
126-
}`);
127-
expect(result.errors).toBeUndefined();
128-
const docsFile = result.data!.allFiles.nodes.find(
129-
n => n.filename === 'contract.pdf'
130-
);
131-
expect(docsFile).toBeDefined();
132-
expect(docsFile!.pathTree).toBe('projects.alpha.docs');
133112
expect(docsFile!.path).toBe('/projects/alpha/docs');
134113
});
135114

@@ -150,26 +129,25 @@ describe('graphile-ltree', () => {
150129
it('works on other tables with ltree columns', async () => {
151130
const result = await query<AllCategoriesResult>(`{
152131
allCategories {
153-
nodes { name treePath treePathTree }
132+
nodes { name treePath }
154133
}
155134
}`);
156135
expect(result.errors).toBeUndefined();
157136
const laptops = result.data!.allCategories.nodes.find(
158137
n => n.name === 'Laptops'
159138
);
160139
expect(laptops).toBeDefined();
161-
expect(laptops!.treePathTree).toBe('shop.electronics.laptops');
162140
expect(laptops!.treePath).toBe('/shop/electronics/laptops');
163141
});
164142
});
165143

166-
// ─── Raw ltree operators (on pathTree) ────────────────────────────────
144+
// ─── Filter operators (slash-delimited input) ──────────────────────────
167145

168-
describe('isAncestorOf filter', () => {
169-
it('finds files under a given path', async () => {
146+
describe('within filter', () => {
147+
it('finds files within a folder using slash paths', async () => {
170148
const result = await query<AllFilesResult>(`{
171-
allFiles(where: { pathTree: { isAncestorOf: "projects.alpha" } }) {
172-
nodes { filename }
149+
allFiles(where: { path: { within: "/projects/alpha" } }) {
150+
nodes { filename path }
173151
}
174152
}`);
175153
expect(result.errors).toBeUndefined();
@@ -184,37 +162,37 @@ describe('graphile-ltree', () => {
184162

185163
it('includes the exact path itself', async () => {
186164
const result = await query<AllFilesResult>(`{
187-
allFiles(where: { pathTree: { isAncestorOf: "projects.alpha" } }) {
188-
nodes { filename pathTree }
165+
allFiles(where: { path: { within: "/projects/alpha" } }) {
166+
nodes { filename path }
189167
}
190168
}`);
191169
expect(result.errors).toBeUndefined();
192-
const paths = result.data!.allFiles.nodes.map(n => n.pathTree);
193-
expect(paths).toContain('projects.alpha');
170+
const paths = result.data!.allFiles.nodes.map(n => n.path);
171+
expect(paths).toContain('/projects/alpha');
194172
});
195173
});
196174

197-
describe('isDescendantOf filter', () => {
198-
it('finds ancestors of a given path', async () => {
175+
describe('ancestorOf filter', () => {
176+
it('finds ancestor folders using slash paths', async () => {
199177
const result = await query<AllFilesResult>(`{
200-
allFiles(where: { pathTree: { isDescendantOf: "projects.alpha.docs.images" } }) {
201-
nodes { filename pathTree }
178+
allFiles(where: { path: { ancestorOf: "/projects/alpha/docs/images" } }) {
179+
nodes { filename path }
202180
}
203181
}`);
204182
expect(result.errors).toBeUndefined();
205-
const paths = result.data!.allFiles.nodes.map(n => n.pathTree);
206-
expect(paths).toContain('projects.alpha.docs.images');
207-
expect(paths).toContain('projects.alpha.docs');
208-
expect(paths).toContain('projects.alpha');
209-
expect(paths).toContain('projects');
183+
const paths = result.data!.allFiles.nodes.map(n => n.path);
184+
expect(paths).toContain('/projects/alpha/docs/images');
185+
expect(paths).toContain('/projects/alpha/docs');
186+
expect(paths).toContain('/projects/alpha');
187+
expect(paths).toContain('/projects');
210188
});
211189
});
212190

213-
describe('matchesGlob filter', () => {
214-
it('matches single-level wildcard', async () => {
191+
describe('glob filter', () => {
192+
it('matches single-level wildcard with slash paths', async () => {
215193
const result = await query<AllFilesResult>(`{
216-
allFiles(where: { pathTree: { matchesGlob: "projects.*" } }) {
217-
nodes { filename pathTree }
194+
allFiles(where: { path: { glob: "/projects/*" } }) {
195+
nodes { filename path }
218196
}
219197
}`);
220198
expect(result.errors).toBeUndefined();
@@ -224,10 +202,10 @@ describe('graphile-ltree', () => {
224202
expect(filenames).not.toContain('contract.pdf');
225203
});
226204

227-
it('matches multi-level wildcard', async () => {
205+
it('matches multi-level wildcard with slash paths', async () => {
228206
const result = await query<AllFilesResult>(`{
229-
allFiles(where: { pathTree: { matchesGlob: "projects.*.docs" } }) {
230-
nodes { filename pathTree }
207+
allFiles(where: { path: { glob: "/projects/*/docs" } }) {
208+
nodes { filename path }
231209
}
232210
}`);
233211
expect(result.errors).toBeUndefined();
@@ -238,67 +216,31 @@ describe('graphile-ltree', () => {
238216
});
239217
});
240218

241-
// ─── Folder operators (slash-path interface, on pathTree) ─────────────
219+
// ─── Slash-to-ltree conversion (round-trip) ────────────────────────────
242220

243-
describe('within filter (folder operator)', () => {
244-
it('finds files within a folder using slash paths', async () => {
221+
describe('slash-to-ltree conversion', () => {
222+
it('accepts slash paths on input and stores as ltree', async () => {
245223
const result = await query<AllFilesResult>(`{
246-
allFiles(where: { pathTree: { within: "/projects/alpha" } }) {
224+
allFiles(where: { path: { within: "/users" } }) {
247225
nodes { filename path }
248226
}
249227
}`);
250228
expect(result.errors).toBeUndefined();
251229
const filenames = result.data!.allFiles.nodes.map(n => n.filename);
252-
expect(filenames).toContain('alpha-spec.pdf');
253-
expect(filenames).toContain('contract.pdf');
254-
expect(filenames).toContain('design.png');
255-
expect(filenames).toContain('budget.xlsx');
256-
expect(filenames).not.toContain('beta-spec.pdf');
257-
expect(filenames).not.toContain('root.txt');
230+
expect(filenames).toContain('avatar.jpg');
231+
expect(filenames).toContain('notes.txt');
258232
});
259-
});
260233

261-
describe('ancestorOf filter (folder operator)', () => {
262-
it('finds ancestor folders using slash paths', async () => {
234+
it('dot-delimited input still works (backward compat)', async () => {
263235
const result = await query<AllFilesResult>(`{
264-
allFiles(where: { pathTree: { ancestorOf: "/projects/alpha/docs/images" } }) {
265-
nodes { filename path }
266-
}
267-
}`);
268-
expect(result.errors).toBeUndefined();
269-
const folders = result.data!.allFiles.nodes.map(n => n.path);
270-
expect(folders).toContain('/projects/alpha/docs/images');
271-
expect(folders).toContain('/projects/alpha/docs');
272-
expect(folders).toContain('/projects/alpha');
273-
expect(folders).toContain('/projects');
274-
});
275-
});
276-
277-
describe('glob filter (folder operator)', () => {
278-
it('matches single-level wildcard with slash paths', async () => {
279-
const result = await query<AllFilesResult>(`{
280-
allFiles(where: { pathTree: { glob: "/projects/*" } }) {
236+
allFiles(where: { path: { within: "projects.alpha" } }) {
281237
nodes { filename path }
282238
}
283239
}`);
284240
expect(result.errors).toBeUndefined();
285241
const filenames = result.data!.allFiles.nodes.map(n => n.filename);
286242
expect(filenames).toContain('alpha-spec.pdf');
287-
expect(filenames).toContain('beta-spec.pdf');
288-
expect(filenames).not.toContain('contract.pdf');
289-
});
290-
291-
it('matches multi-level wildcard with slash paths', async () => {
292-
const result = await query<AllFilesResult>(`{
293-
allFiles(where: { pathTree: { glob: "/projects/*/docs" } }) {
294-
nodes { filename path }
295-
}
296-
}`);
297-
expect(result.errors).toBeUndefined();
298-
const filenames = result.data!.allFiles.nodes.map(n => n.filename);
299243
expect(filenames).toContain('contract.pdf');
300-
expect(filenames).toContain('proposal.docx');
301-
expect(filenames).not.toContain('design.png');
302244
});
303245
});
304246
});
Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
/**
22
* graphile-ltree — PostGraphile v5 ltree Plugin
33
*
4-
* Two presets:
5-
*
6-
* - `GraphileLtreePreset` — base ltree operators (isAncestorOf, isDescendantOf, matchesGlob)
7-
* - `GraphileFolderPreset` — folder-oriented layer (within, ancestorOf, glob) + pathFolder fields
4+
* File-path syntax for ltree columns. The LTree scalar auto-converts between
5+
* slash-delimited file paths ("/projects/alpha/docs") in the GraphQL API and
6+
* dot-delimited ltree ("projects.alpha.docs") in PostgreSQL.
87
*
98
* @example
109
* ```typescript
11-
* // For folder-style interface (recommended):
12-
* import { GraphileFolderPreset } from 'graphile-ltree';
13-
*
14-
* // For raw ltree operators only:
1510
* import { GraphileLtreePreset } from 'graphile-ltree';
1611
* ```
1712
*/
@@ -22,9 +17,12 @@ export { GraphileFolderPreset, GraphileLtreePreset } from './preset';
2217
// Individual plugins
2318
export type { LtreeExtensionInfo } from './plugins/detect-ltree';
2419
export { LtreeExtensionDetectionPlugin } from './plugins/detect-ltree';
25-
export { LtreeFolderFieldPlugin } from './plugins/folder-field';
26-
export { LTREE_SCALAR_NAME, LtreeCodecPlugin } from './plugins/ltree-codec';
20+
export { ltreeToSlash, LTREE_SCALAR_NAME, LtreeCodecPlugin, slashToLtree } from './plugins/ltree-codec';
2721

2822
// Connection filter operator factories
29-
export { createLtreeOperatorFactory } from './plugins/connection-filter-operators';
3023
export { createFolderOperatorFactory } from './plugins/folder-filter-operators';
24+
25+
// Deprecated — folder field plugin is no longer needed (scalar handles conversion)
26+
export { LtreeFolderFieldPlugin } from './plugins/folder-field';
27+
// Deprecated — use createFolderOperatorFactory instead (better operator names)
28+
export { createLtreeOperatorFactory } from './plugins/connection-filter-operators';

0 commit comments

Comments
 (0)