Skip to content

Commit 7d2e4e5

Browse files
committed
feat: friendly path for records
1 parent 41d0a32 commit 7d2e4e5

35 files changed

+1615
-1166
lines changed

docs/src/guide/cluster-types.md

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ Cluster types support a few options that can be passed during their declaration.
101101

102102
```ts
103103
type ClusterTypesOptions = {
104-
autocomplete?: 'strict' | 'friendly'; // default: 'friendly'
104+
codeCompletion?: {
105+
array?: 'friendly' | 'strict'; // default: 'friendly'
106+
record?: 'friendly' | 'strict'; // default: 'friendly'
107+
recordPlaceholder?: string; // default: '#'
108+
};
105109
keyMatchingStrategy?: 'always' | 'firstMatch'; // default: 'always'
106110
keyDelimiter?: string;
107111
}
@@ -218,10 +222,10 @@ type MyClusterTypes = {
218222
};
219223
```
220224

221-
### Autocomplete
225+
### Code completion
222226

223-
There is a limitation of TypeScript regarding autocomplete when the path is a template string like `authors[${number}]`, the autocomplete will not offer that value.
224-
Because of that, Cbjs adds _friendly paths_ to the autocomplete list :
227+
There is a limitation of TypeScript regarding code completion when the path is a template string like `authors[${number}]`, the typescript service will not offer that value.
228+
Because of that, Cbjs adds _friendly paths_ to the completion list :
225229

226230
```ts twoslash
227231
import { connect, DocDef } from '@cbjsdev/cbjs';
@@ -245,13 +249,16 @@ const cluster = await connect<MyClusterTypes>('');
245249
const collection = cluster.bucket('store').scope('library').collection('books');
246250

247251
// ---cut-before---
248-
// @noErrors: 2769
252+
// @noErrors: 2345 2769
249253
const result = await collection
250254
.lookupIn('book::001')
251255
.get('autho');
252256
// ^|
253257
```
254258

259+
&nbsp;
260+
&nbsp;
261+
255262
Because their length is fixed, all indexes will be offered for tuples.
256263

257264
```ts twoslash
@@ -276,7 +283,7 @@ const cluster = await connect<MyClusterTypes>('');
276283
const collection = cluster.bucket('store').scope('library').collection('books');
277284

278285
// ---cut-before---
279-
// @noErrors: 2769
286+
// @noErrors: 2345 2769
280287
const result = await collection
281288
.lookupIn('book::001')
282289
.get('quater_sal');
@@ -286,9 +293,62 @@ const result = await collection
286293
&nbsp;
287294
&nbsp;
288295
&nbsp;
289-
&nbsp;
296+
&nbsp;
297+
&nbsp;
298+
299+
300+
For records, a placeholder will be injected where the key is expected :
301+
302+
```ts twoslash
303+
import { connect, DocDef } from '@cbjsdev/cbjs';
304+
305+
type MyClusterTypes = {
306+
store: {
307+
library: {
308+
books: [ DocDef<
309+
`book::${string}`,
310+
{
311+
editions: Record<`edition::${string}`, { name: string; firstRelease: number }>
312+
}
313+
> ]
314+
};
315+
};
316+
};
317+
318+
const cluster = await connect<MyClusterTypes>('');
319+
const collection = cluster.bucket('store').scope('library').collection('books');
320+
321+
// ---cut-before---
322+
// @noErrors: 2345 2769
323+
const result = await collection
324+
.lookupIn('book::001')
325+
.get('edit');
326+
// ^|
327+
```
290328

291-
You can turn off friendly path in the cluster types options :
329+
&nbsp;
330+
&nbsp;
331+
&nbsp;
332+
&nbsp;
333+
334+
::: tip
335+
Use specific key type such as `edition::${string}` instead of simply `string` to benefit from friendly paths.
336+
Wide type like `string` will match the placeholder and will be swallowed.
337+
:::
338+
339+
To change the placeholder or turn off friendly path completely, set the options the cluster types definitions :
340+
341+
```ts
342+
type MyClusterTypes = {
343+
'@options': {
344+
codeCompletion: {
345+
array: 'strict'; // default 'friendly'
346+
record: 'strict'; // default 'friendly'
347+
recordPlaceholder: '!'; // default '#'
348+
}
349+
}
350+
};
351+
```
292352

293353
## Incremental adoption
294354

docs/src/guide/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ type MyClusterTypes = {
8181
const cluster = await connect<MyClusterTypes>('');
8282
const collection = cluster.bucket('store').scope('library').collection('books');
8383
const bookId = 'book::001';
84-
// @noErrors: 2769
84+
// @noErrors: 2345 2769
8585
// ---cut-before---
8686
const {
8787
content: [title],

packages/cbjs/src/clusterTypes/clusterTypes.spec-d.ts

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { describe, expectTypeOf, it } from 'vitest';
1717

1818
import {
1919
CollectionName,
20-
CouchbaseClusterTypes,
2120
DefaultClusterTypes,
2221
IsDefaultClusterTypes,
2322
LookupInMacroShape,
@@ -34,7 +33,6 @@ import {
3433
CollectionContainingDocDef,
3534
CollectionMatchingDocDef,
3635
DocDef,
37-
PathAutocomplete,
3836
} from './clusterTypes.js';
3937
import { LookupInGetPath } from './kv/lookup/lookupOperations.types.js';
4038

@@ -363,78 +361,3 @@ describe('CollectionContainingDocBody', () => {
363361
>().toEqualTypeOf<Collection<UserClusterTypes, 'store', 'library', 'books'>>();
364362
});
365363
});
366-
367-
describe('PathAutocomplete', () => {
368-
type BookDocDef = DocDef<`book::${string}`, { metadata: { tags: string[] } }>;
369-
370-
it('should offer friendly paths as is when no options are provided', () => {
371-
type ClusterTypesWithArrays = {
372-
b: { s: { c: [BookDocDef] } };
373-
};
374-
375-
type UserCollection = Collection<ClusterTypesWithArrays, 'b', 's', 'c'>;
376-
377-
expectTypeOf<
378-
PathAutocomplete<UserCollection, Exclude<LookupInGetPath<BookDocDef>, ''>>
379-
>().toEqualTypeOf<
380-
| 'metadata'
381-
| 'metadata.tags'
382-
| `metadata.tags[${number}]`
383-
| 'metadata.tags[]'
384-
| LookupInMacroShape
385-
>();
386-
});
387-
388-
it('should offer friendly paths as is when options are set but no the autocomplete property', () => {
389-
type ClusterTypesWithArrays = {
390-
'@options': { keyMatchingStrategy: 'always' };
391-
'b': { s: { c: [BookDocDef] } };
392-
};
393-
394-
type UserCollection = Collection<ClusterTypesWithArrays, 'b', 's', 'c'>;
395-
396-
expectTypeOf<
397-
PathAutocomplete<UserCollection, Exclude<LookupInGetPath<BookDocDef>, ''>>
398-
>().toEqualTypeOf<
399-
| 'metadata'
400-
| 'metadata.tags'
401-
| `metadata.tags[${number}]`
402-
| 'metadata.tags[]'
403-
| LookupInMacroShape
404-
>();
405-
});
406-
407-
it('should offer friendly paths as is when the option is set to friendly', () => {
408-
type ClusterTypesWithArrays = {
409-
'@options': { autocomplete: 'friendly' };
410-
'b': { s: { c: [BookDocDef] } };
411-
};
412-
413-
type UserCollection = Collection<ClusterTypesWithArrays, 'b', 's', 'c'>;
414-
415-
expectTypeOf<
416-
PathAutocomplete<UserCollection, Exclude<LookupInGetPath<BookDocDef>, ''>>
417-
>().toEqualTypeOf<
418-
| 'metadata'
419-
| 'metadata.tags'
420-
| `metadata.tags[${number}]`
421-
| 'metadata.tags[]'
422-
| LookupInMacroShape
423-
>();
424-
});
425-
426-
it('should offer strict paths as is when the option is set to strict', () => {
427-
type ClusterTypesWithArrays = {
428-
'@options': { autocomplete: 'strict' };
429-
'b': { s: { c: [BookDocDef] } };
430-
};
431-
432-
type UserCollection = Collection<ClusterTypesWithArrays, 'b', 's', 'c'>;
433-
434-
expectTypeOf<
435-
PathAutocomplete<UserCollection, Exclude<LookupInGetPath<BookDocDef>, ''>>
436-
>().toEqualTypeOf<
437-
'metadata' | 'metadata.tags' | `metadata.tags[${number}]` | LookupInMacroShape
438-
>();
439-
});
440-
});

packages/cbjs/src/clusterTypes/clusterTypes.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
DocDef,
2727
DocDefKeyBodyShape,
2828
DocDefMatchingKey,
29-
FriendlyPathToArrayIndex,
3029
GetKeyspaceOptions,
3130
IsNever,
3231
JsonDocumentDef,
@@ -44,23 +43,6 @@ import type { Scope } from '../scope.js';
4443

4544
export type { DocDef };
4645

47-
/**
48-
* Add friendly paths that are autocomplete friendly, is configured that way.
49-
* TODO incorporate this into the DocumentCodeCompletion
50-
*/
51-
// prettier-ignore
52-
export type PathAutocomplete<Instance, Path> =
53-
Instance extends Collection<infer T extends CouchbaseClusterTypes, infer B, infer S, infer C> ?
54-
GetKeyspaceOptions<T, B, S, C> extends infer Options extends ClusterTypesOptions ?
55-
Options extends { autocomplete?: infer Autocomplete } ?
56-
Autocomplete extends 'friendly' ?
57-
FriendlyPathToArrayIndex<Path> :
58-
Path :
59-
Path :
60-
never :
61-
never
62-
;
63-
6446
// prettier-ignore
6547
export type CollectionDocDef<Instance> =
6648
Instance extends Collection<infer T, infer B, infer S, infer C> ?
@@ -143,6 +125,13 @@ export type ClusterTypesWith<
143125
};
144126
};
145127

128+
// prettier-ignore
129+
export type CollectionOptions<Instance> =
130+
Instance extends Collection<infer T, infer B, infer S, infer C> ?
131+
GetKeyspaceOptions<T, B, S, C> :
132+
never
133+
;
134+
146135
export type AnyBucket = Bucket<any, any>;
147136
export type AnyScope = Scope<any, any, any>;
148137
export type AnyCollection = Collection<any, any, any, any>;

packages/cbjs/src/clusterTypes/kv/crud.spec-d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
*/
1616
import { describe, expectTypeOf, it } from 'vitest';
1717

18-
import { FriendlyPathToArrayIndex } from '@cbjsdev/shared';
19-
2018
import { connect } from '../../couchbase.js';
2119
import {
2220
GetReplicaResult,
@@ -27,7 +25,7 @@ import {
2725
import { PrefixScan, SamplingScan } from '../../rangeScan.js';
2826
import { LookupInSpec } from '../../sdspecs.js';
2927
import { StreamableReplicasPromise } from '../../streamablepromises.js';
30-
import { DocDef, PathAutocomplete } from '../clusterTypes.js';
28+
import { DocDef } from '../clusterTypes.js';
3129

3230
type Book = { title: string };
3331
type QuarterSales = { sales: number[] };

packages/cbjs/src/clusterTypes/kv/lookup/lookupIn.types.spec-d.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
LookupInResultEntry,
2828
} from '../../../index.js';
2929
import { LookupInSpec } from '../../../sdspecs.js';
30+
import { CollectionOptions } from '../../clusterTypes.js';
3031
import {
3132
LookupInInternalPath,
3233
LookupInResultEntries,
@@ -35,13 +36,16 @@ import {
3536
LookupInSpecResults,
3637
} from './lookupIn.types.js';
3738

39+
type NoOptions = NonNullable<unknown>;
40+
3841
describe('LookupInSpecs', () => {
3942
type TestDoc = {
4043
title: string;
4144
metadata: {
4245
tags: string[];
4346
};
4447
authors: [string, ...string[]];
48+
editions: Record<`edition::${string}`, { publishedAt: number }>;
4549
};
4650

4751
type TestDoc2 = {
@@ -258,11 +262,26 @@ describe('LookupInSpecs', () => {
258262
]);
259263
});
260264

261-
it('should offer friendly autocomplete values for array indexes', async () => {
265+
it('should offer friendly code completion path for array indexes', async () => {
266+
const cluster = await connect<UserClusterTypes>('couchbase://127.0.0.1');
267+
const collection = cluster.bucket('test').defaultCollection();
268+
269+
const result = await collection
270+
.lookupIn('test__document')
271+
.get('metadata.sales[]');
272+
273+
expectTypeOf(result).toEqualTypeOf<LookupInResult<[never]>>();
274+
});
275+
276+
it('should offer friendly code completion path for record keys', async () => {
262277
const cluster = await connect<UserClusterTypes>('couchbase://127.0.0.1');
263278
const collection = cluster.bucket('test').defaultCollection();
264279

265-
const result = await collection.lookupIn('test__document').get('');
280+
const result = await collection
281+
.lookupIn('test__document')
282+
.get('editions.#.publishedAt');
283+
284+
expectTypeOf(result).toEqualTypeOf<LookupInResult<[never]>>();
266285
});
267286

268287
it('should infer the result type of an array of typeless specs based on collection documents', async () => {
@@ -307,9 +326,9 @@ describe('LookupInSpecs', () => {
307326

308327
describe('LookupInSpecResult', () => {
309328
type Test<
310-
Path extends LookupInInternalPath<TestDocDef, Opcode>,
329+
Path extends LookupInInternalPath<NoOptions, TestDocDef, Opcode>,
311330
Opcode extends LookupInSpecOpCode,
312-
> = LookupInSpecResult<LookupInSpec<TestDocDef, Opcode, Path>, TestDocDef>;
331+
> = LookupInSpecResult<NoOptions, LookupInSpec<TestDocDef, Opcode, Path>, TestDocDef>;
313332

314333
it('should infer the correct type', () => {
315334
expectTypeOf<
@@ -342,6 +361,7 @@ describe('LookupInSpecs', () => {
342361
describe('LookupInSpecResults', () => {
343362
it('should infer the correct result when using user defined document', () => {
344363
type Test = LookupInSpecResults<
364+
NoOptions,
345365
[
346366
LookupInSpec<TestDocDef, CppProtocolSubdocOpcode.get, 'title'>,
347367
LookupInSpec<TestDocDef, CppProtocolSubdocOpcode.exists, 'title'>,
@@ -358,6 +378,7 @@ describe('LookupInSpecs', () => {
358378
// prettier-ignore
359379
expectTypeOf<
360380
LookupInSpecResults<
381+
NoOptions,
361382
[
362383
LookupInSpec<AnyDocDef, CppProtocolSubdocOpcode.get, 'title'>,
363384
LookupInSpec<AnyDocDef, CppProtocolSubdocOpcode.exists, 'title'>,
@@ -372,6 +393,7 @@ describe('LookupInSpecs', () => {
372393
// prettier-ignore
373394
expectTypeOf<
374395
LookupInSpecResults<
396+
NoOptions,
375397
[
376398
LookupInSpec<DocDef, CppProtocolSubdocOpcode.get, 'title'>,
377399
LookupInSpec<DocDef, CppProtocolSubdocOpcode.exists, 'title'>,

0 commit comments

Comments
 (0)