diff --git a/.changeset/moody-dots-drum.md b/.changeset/moody-dots-drum.md new file mode 100644 index 000000000000..a89bdddcf4a2 --- /dev/null +++ b/.changeset/moody-dots-drum.md @@ -0,0 +1,13 @@ +--- +'@data-client/rest': minor +--- + +RestEndpoint.path and Resource.path syntax updated + +Upgrading path-to-regexp from 6 to 8. +- https://github.com/pillarjs/path-to-regexp/releases/tag/v8.0.0 +- https://github.com/pillarjs/path-to-regexp/releases/tag/v7.0.0 + +BREAKING CHANGES: +- /:optional? -> {/:optional} +- \ No newline at end of file diff --git a/__tests__/new.ts b/__tests__/new.ts index 043d518da187..bec3691676e1 100644 --- a/__tests__/new.ts +++ b/__tests__/new.ts @@ -1,3 +1,6 @@ +import { Temporal } from '@js-temporal/polyfill'; +import React, { createContext, useContext } from 'react'; + import { schema, Endpoint, @@ -12,8 +15,6 @@ import { Resource, ResourceOptions, } from '@data-client/rest'; -import { Temporal } from '@js-temporal/polyfill'; -import React, { createContext, useContext } from 'react'; /** Represents data with primary key being from 'id' field. */ export class IDEntity extends Entity { @@ -358,14 +359,14 @@ const CoolerArticleResourceBase = createArticleResource({ export const CoolerArticleResource = { ...CoolerArticleResourceBase, get: CoolerArticleResourceBase.get.extend({ - path: '/:id?/:title?', + path: '{/:id}{/:title}', }), }; export const OptimisticArticleResource = createArticleResource({ schema: CoolerArticle, urlRoot: 'article-cooler', optimistic: true, -}).extend('get', { path: '/:id?/:title?' }); +}).extend('get', { path: '{/:id}{/:title}' }); const CoolerArticleResourceFromMixinBase = createArticleResource({ schema: ArticleFromMixin, @@ -374,7 +375,7 @@ const CoolerArticleResourceFromMixinBase = createArticleResource({ export const CoolerArticleResourceFromMixin = { ...CoolerArticleResourceFromMixinBase, get: CoolerArticleResourceFromMixinBase.get.extend({ - path: '/:id?/:title?', + path: '{/:id}{/:title}', }), }; diff --git a/packages/rest/package.json b/packages/rest/package.json index 54e230ff65f9..34dbb06c7848 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -132,7 +132,7 @@ "dependencies": { "@babel/runtime": "^7.17.0", "@data-client/endpoint": "^0.14.12", - "path-to-regexp": "^6.3.0" + "path-to-regexp": "^8.1.0" }, "devDependencies": { "@anansi/browserslist-config": "^1.4.2", diff --git a/packages/rest/src/RestHelpers.ts b/packages/rest/src/RestHelpers.ts index d89c3b90c219..f391bbef1f5c 100644 --- a/packages/rest/src/RestHelpers.ts +++ b/packages/rest/src/RestHelpers.ts @@ -1,14 +1,11 @@ -import { compile, PathFunction, parse } from 'path-to-regexp'; +import { compile, PathFunction, parse, Token, ParamData } from 'path-to-regexp'; import { ShortenPath } from './pathTypes.js'; const urlBaseCache: Record> = Object.create(null); -export function getUrlBase(path: string): PathFunction { +export function getUrlBase(path: string): PathFunction { if (!(path in urlBaseCache)) { - urlBaseCache[path] = compile(path, { - encode: encodeURIComponent, - validate: false, - }); + urlBaseCache[path] = compile(path); } return urlBaseCache[path]; } @@ -16,13 +13,26 @@ export function getUrlBase(path: string): PathFunction { const urlTokensCache: Record> = Object.create(null); export function getUrlTokens(path: string): Set { if (!(path in urlTokensCache)) { - urlTokensCache[path] = new Set( - parse(path).map(t => (typeof t === 'string' ? t : `${t['name']}`)), - ); + urlTokensCache[path] = tokenMap(parse(path).tokens); } return urlTokensCache[path]; } +function tokenMap(tokens: Token[]): Set { + const tokenNames = new Set(); + tokens.forEach(token => { + switch (token.type) { + case 'param': + case 'wildcard': + tokenNames.add(token.name); + break; + case 'group': + return tokenNames.union(tokenMap(token.tokens)); + } + }); + return tokenNames; +} + const proto = Object.prototype; const gpo = Object.getPrototypeOf; diff --git a/packages/rest/src/__tests__/RestEndpoint.ts b/packages/rest/src/__tests__/RestEndpoint.ts index 17baae05355e..1ae9f1204556 100644 --- a/packages/rest/src/__tests__/RestEndpoint.ts +++ b/packages/rest/src/__tests__/RestEndpoint.ts @@ -208,9 +208,9 @@ describe('RestEndpoint', () => { }); it('only optional path means the arg is not required', () => { - const ep = new RestEndpoint({ path: '/users/:id?/:group?' }); + const ep = new RestEndpoint({ path: '/users{/:id}{/:group}' }); const epbody = new RestEndpoint({ - path: '/users/:id?/:group?', + path: '/users{/:id}{/:group}', body: { title: '' }, method: 'POST', }); @@ -1369,7 +1369,6 @@ describe('RestEndpoint.fetch()', () => { expect(error).toBeDefined(); expect(error.status).toBe(500); - // eslint-disable-next-line require-atomic-updates console.error = oldError; }); diff --git a/packages/rest/src/pathTypes.ts b/packages/rest/src/pathTypes.ts index 22515b32e3ff..ee377fd10d05 100644 --- a/packages/rest/src/pathTypes.ts +++ b/packages/rest/src/pathTypes.ts @@ -1,8 +1,5 @@ -type OnlyOptional = - S extends `${infer K}}?` ? K - : S extends `${infer K}?` ? K - : never; -type OnlyRequired = S extends `${string}?` ? never : S; +type OnlyOptional = S extends `${infer K}}` ? K : never; +type OnlyRequired = S extends `${string}}` ? never : S; /** Parameters for a given path */ export type PathArgs = @@ -14,18 +11,18 @@ export type PathArgs = /** Computes the union of keys for a path string */ export type PathKeys = string extends S ? string - : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? + : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; type PathSplits = S extends ( - `${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}` + `${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}` ) ? - PathSplits<`:${K}`> | PathSplits - : S extends `${string}:${infer K}:${infer R}` ? - PathSplits<`:${K}`> | PathSplits<`:${R}`> - : S extends `${string}:${infer K}` ? K + PathSplits<`${':' | '*'}${K}`> | PathSplits + : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? + PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> + : S extends `${string}${':' | '*'}${infer K}` ? K : never; export type KeysToArgs = { diff --git a/packages/rest/typescript-tests/types.test.ts b/packages/rest/typescript-tests/types.test.ts index a24ee96cc8f9..05d9ad23ff66 100644 --- a/packages/rest/typescript-tests/types.test.ts +++ b/packages/rest/typescript-tests/types.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Entity, schema } from '@data-client/endpoint'; import { useController, useSuspense } from '@data-client/react'; import { User } from '__tests__/new'; @@ -197,7 +196,7 @@ it('should precisely type function arguments', () => { // path: '/todos/:id?' () => { const optionalUndefSearch = new RestEndpoint({ - path: '/todos/:id?', + path: '/todos{/:id}', searchParams: {} as | { userId?: string | number; @@ -205,23 +204,23 @@ it('should precisely type function arguments', () => { | undefined, }); const optionalSearch = new RestEndpoint({ - path: '/todos/:id?', + path: '/todos{/:id}', searchParams: {} as { userId?: string | number; }, }); const undef = new RestEndpoint({ - path: '/todos/:id?', + path: '/todos{/:id}', searchParams: undefined, }); const requiredSearch = new RestEndpoint({ - path: '/todos/:id?', + path: '/todos{/:id}', searchParams: {} as { userId: string | number; }, }); const noSearch = new RestEndpoint({ - path: '/todos/:id?', + path: '/todos{/:id}', }); () => optionalUndefSearch(); () => optionalUndefSearch({}); @@ -573,20 +572,19 @@ it('should handle more open ended type definitions', () => { () => { const getThing = new RestEndpoint({ - path: '/:id*:bob', + path: '{/*id}:bob', }); getThing({ id: 5, bob: 'hi' }); // @ts-expect-error getThing({ id: 'hi' }); - // @ts-expect-error getThing({ bob: 'hi' }); // @ts-expect-error getThing(5); }; () => { const getThing = new RestEndpoint({ - path: '/:id+:bob', + path: '/*id:bob', }); getThing({ id: 5, bob: 'hi' }); @@ -601,7 +599,7 @@ it('should handle more open ended type definitions', () => { }; () => { const getThing = new RestEndpoint({ - path: '/:id\\+:bob', + path: '/:id\\,:bob', }); getThing({ id: 5, bob: 'hi' }); @@ -616,7 +614,7 @@ it('should handle more open ended type definitions', () => { }; () => { const getThing = new RestEndpoint({ - path: '/:id:bob+', + path: '/:id/*bob', }); getThing({ id: 5, bob: 'hi' }); @@ -631,7 +629,7 @@ it('should handle more open ended type definitions', () => { }; () => { const getThing = new RestEndpoint({ - path: '/:foo/(.*)', + path: '/:foo/(.)', }); getThing({ foo: 'hi' }); @@ -644,7 +642,7 @@ it('should handle more open ended type definitions', () => { }; () => { const getThing = new RestEndpoint({ - path: '/:attr1?{-:attr2}?{-:attr3}?', + path: '{/:attr1}{-:attr2}{-:attr3}', }); getThing({ attr1: 'hi' }); diff --git a/website/src/components/Playground/editor-types/@data-client/core.d.ts b/website/src/components/Playground/editor-types/@data-client/core.d.ts index 18c49e12bcc0..ef076d2d7d84 100644 --- a/website/src/components/Playground/editor-types/@data-client/core.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/core.d.ts @@ -485,14 +485,14 @@ declare class Controller { */ fetch: ; - }>(endpoint: E, ...args_0: Parameters) => E["schema"] extends undefined | null ? ReturnType : Promise>; + }>(endpoint: E, ...args: Parameters) => E["schema"] extends undefined | null ? ReturnType : Promise>; /** * Fetches only if endpoint is considered 'stale'; otherwise returns undefined * @see https://dataclient.io/docs/api/Controller#fetchIfStale */ fetchIfStale: ; - }>(endpoint: E, ...args_0: Parameters) => E["schema"] extends undefined | null ? ReturnType | ResolveType : Promise> | Denormalize; + }>(endpoint: E, ...args: Parameters) => E["schema"] extends undefined | null ? ReturnType | ResolveType : Promise> | Denormalize; /** * Forces refetching and suspense on useSuspense with the same Endpoint and parameters. * @see https://dataclient.io/docs/api/Controller#invalidate diff --git a/website/src/components/Playground/editor-types/@data-client/rest.d.ts b/website/src/components/Playground/editor-types/@data-client/rest.d.ts index 946b1969e4d7..81be6547d25e 100644 --- a/website/src/components/Playground/editor-types/@data-client/rest.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/rest.d.ts @@ -1,4 +1,4 @@ -import { PathFunction } from 'path-to-regexp'; +import { PathFunction, ParamData } from 'path-to-regexp'; interface NetworkError$1 extends Error { status: number; @@ -1163,13 +1163,13 @@ type ExtractCollection = S extends ({ [K: string]: Schema; } ? ExtractObject : never; -type OnlyOptional = S extends `${infer K}}?` ? K : S extends `${infer K}?` ? K : never; -type OnlyRequired = S extends `${string}?` ? never : S; +type OnlyOptional = S extends `${infer K}}` ? K : never; +type OnlyRequired = S extends `${string}}` ? never : S; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; /** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; -type PathSplits = S extends (`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`) ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; +type PathSplits = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never; type KeysToArgs = { [K in Key as OnlyOptional]?: string | number; } & (OnlyRequired extends never ? unknown : { @@ -1533,7 +1533,7 @@ type MutateEndpoint; declare function getUrlTokens(path: string): Set; type ResourceExtension = S extends ({ [K: string]: Schema; } ? ExtractObject : never; -type OnlyOptional = S extends `${infer K}}?` ? K : S extends `${infer K}?` ? K : never; -type OnlyRequired = S extends `${string}?` ? never : S; +type OnlyOptional = S extends `${infer K}}` ? K : never; +type OnlyRequired = S extends `${string}}` ? never : S; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; /** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; -type PathSplits = S extends (`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`) ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; +type PathSplits = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never; type KeysToArgs = { [K in Key as OnlyOptional]?: string | number; } & (OnlyRequired extends never ? unknown : { @@ -1537,7 +1537,7 @@ type MutateEndpoint; declare function getUrlTokens(path: string): Set; type ResourceExtension