Skip to content

Commit

Permalink
pkg: Update path-to-regexp to v8
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Sep 13, 2024
1 parent d954ac3 commit 0f9f55d
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 57 deletions.
17 changes: 17 additions & 0 deletions .changeset/moody-dots-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@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}
- /:repeating+ -> /*repeating
- /:repeating* -> {/*repeating}
- `(`, `)`, `[`, `]` must be escaped `"\\("`
- `()[]{}*:;,!@` are all characters that need escaping
- /:with-dash -> /:"with-dash"
11 changes: 6 additions & 5 deletions __tests__/new.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Temporal } from '@js-temporal/polyfill';
import React, { createContext, useContext } from 'react';

import {
schema,
Endpoint,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -374,7 +375,7 @@ const CoolerArticleResourceFromMixinBase = createArticleResource({
export const CoolerArticleResourceFromMixin = {
...CoolerArticleResourceFromMixinBase,
get: CoolerArticleResourceFromMixinBase.get.extend({
path: '/:id?/:title?',
path: '{/:id}{/:title}',
}),
};

Expand Down
2 changes: 1 addition & 1 deletion packages/rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 19 additions & 9 deletions packages/rest/src/RestHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
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<string, PathFunction<object>> = Object.create(null);
export function getUrlBase(path: string): PathFunction {
export function getUrlBase(path: string): PathFunction<ParamData> {
if (!(path in urlBaseCache)) {
urlBaseCache[path] = compile(path, {
encode: encodeURIComponent,
validate: false,
});
urlBaseCache[path] = compile(path);
}
return urlBaseCache[path];
}

const urlTokensCache: Record<string, Set<string>> = Object.create(null);
export function getUrlTokens(path: string): Set<string> {
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<string> {
const tokenNames = new Set<string>();
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;

Expand Down
5 changes: 2 additions & 3 deletions packages/rest/src/__tests__/RestEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down Expand Up @@ -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;
});

Expand Down
19 changes: 8 additions & 11 deletions packages/rest/src/pathTypes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
type OnlyOptional<S extends string> =
S extends `${infer K}}?` ? K
: S extends `${infer K}?` ? K
: never;
type OnlyRequired<S extends string> = S extends `${string}?` ? never : S;
type OnlyOptional<S extends string> = S extends `${infer K}}` ? K : never;
type OnlyRequired<S extends string> = S extends `${string}}` ? never : S;

/** Parameters for a given path */
export type PathArgs<S extends string> =
Expand All @@ -14,18 +11,18 @@ export type PathArgs<S extends string> =
/** Computes the union of keys for a path string */
export type PathKeys<S extends string> =
string extends S ? string
: S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ?
: S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ?
PathKeys<A> | PathKeys<B>
: PathSplits<S>;

type PathSplits<S extends string> =
S extends (
`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`
`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`
) ?
PathSplits<`:${K}`> | PathSplits<R>
: S extends `${string}:${infer K}:${infer R}` ?
PathSplits<`:${K}`> | PathSplits<`:${R}`>
: S extends `${string}:${infer K}` ? K
PathSplits<`${':' | '*'}${K}`> | PathSplits<R>
: S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ?
PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`>
: S extends `${string}${':' | '*'}${infer K}` ? K
: never;

export type KeysToArgs<Key extends string> = {
Expand Down
24 changes: 11 additions & 13 deletions packages/rest/typescript-tests/types.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -197,31 +196,31 @@ it('should precisely type function arguments', () => {
// path: '/todos/:id?'
() => {
const optionalUndefSearch = new RestEndpoint({
path: '/todos/:id?',
path: '/todos{/:id}',
searchParams: {} as
| {
userId?: string | number;
}
| 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({});
Expand Down Expand Up @@ -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' });
Expand All @@ -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' });
Expand All @@ -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' });
Expand All @@ -631,7 +629,7 @@ it('should handle more open ended type definitions', () => {
};
() => {
const getThing = new RestEndpoint({
path: '/:foo/(.*)',
path: '/:foo/(.)',
});

getThing({ foo: 'hi' });
Expand All @@ -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' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,14 @@ declare class Controller<D extends GenericDispatch = DataClientDispatch> {
*/
fetch: <E extends EndpointInterface & {
update?: EndpointUpdateFunction<E>;
}>(endpoint: E, ...args_0: Parameters<E>) => E["schema"] extends undefined | null ? ReturnType<E> : Promise<Denormalize<E["schema"]>>;
}>(endpoint: E, ...args: Parameters<E>) => E["schema"] extends undefined | null ? ReturnType<E> : Promise<Denormalize<E["schema"]>>;
/**
* Fetches only if endpoint is considered 'stale'; otherwise returns undefined
* @see https://dataclient.io/docs/api/Controller#fetchIfStale
*/
fetchIfStale: <E extends EndpointInterface & {
update?: EndpointUpdateFunction<E>;
}>(endpoint: E, ...args_0: Parameters<E>) => E["schema"] extends undefined | null ? ReturnType<E> | ResolveType<E> : Promise<Denormalize<E["schema"]>> | Denormalize<E["schema"]>;
}>(endpoint: E, ...args: Parameters<E>) => E["schema"] extends undefined | null ? ReturnType<E> | ResolveType<E> : Promise<Denormalize<E["schema"]>> | Denormalize<E["schema"]>;
/**
* Forces refetching and suspense on useSuspense with the same Endpoint and parameters.
* @see https://dataclient.io/docs/api/Controller#invalidate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PathFunction } from 'path-to-regexp';
import { PathFunction, ParamData } from 'path-to-regexp';

interface NetworkError$1 extends Error {
status: number;
Expand Down Expand Up @@ -1163,13 +1163,13 @@ type ExtractCollection<S extends Schema | undefined> = S extends ({
[K: string]: Schema;
} ? ExtractObject<S> : never;

type OnlyOptional<S extends string> = S extends `${infer K}}?` ? K : S extends `${infer K}?` ? K : never;
type OnlyRequired<S extends string> = S extends `${string}?` ? never : S;
type OnlyOptional<S extends string> = S extends `${infer K}}` ? K : never;
type OnlyRequired<S extends string> = S extends `${string}}` ? never : S;
/** Parameters for a given path */
type PathArgs<S extends string> = PathKeys<S> extends never ? unknown : KeysToArgs<PathKeys<S>>;
/** Computes the union of keys for a path string */
type PathKeys<S extends string> = string extends S ? string : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? PathKeys<A> | PathKeys<B> : PathSplits<S>;
type PathSplits<S extends string> = S extends (`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`) ? PathSplits<`:${K}`> | PathSplits<R> : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never;
type PathKeys<S extends string> = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys<A> | PathKeys<B> : PathSplits<S>;
type PathSplits<S extends string> = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits<R> : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never;
type KeysToArgs<Key extends string> = {
[K in Key as OnlyOptional<K>]?: string | number;
} & (OnlyRequired<Key> extends never ? unknown : {
Expand Down Expand Up @@ -1533,7 +1533,7 @@ type MutateEndpoint<O extends {
*/
declare let RestEndpoint: RestEndpointConstructor;

declare function getUrlBase(path: string): PathFunction;
declare function getUrlBase(path: string): PathFunction<ParamData>;
declare function getUrlTokens(path: string): Set<string>;

type ResourceExtension<R extends {
Expand Down
12 changes: 6 additions & 6 deletions website/src/components/Playground/editor-types/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PathFunction } from 'path-to-regexp';
import { PathFunction, ParamData } from 'path-to-regexp';
import { Manager, State, Controller, EndpointInterface as EndpointInterface$1, FetchFunction as FetchFunction$1, Schema as Schema$1, ResolveType as ResolveType$1, Denormalize as Denormalize$1, DenormalizeNullable as DenormalizeNullable$1, Queryable as Queryable$1, NI as NI$1, SchemaArgs as SchemaArgs$1, NetworkError as NetworkError$2, UnknownError as UnknownError$1, ErrorTypes as ErrorTypes$2 } from '@data-client/core';
export { Manager } from '@data-client/core';
import React, { JSX } from 'react';
Expand Down Expand Up @@ -1167,13 +1167,13 @@ type ExtractCollection<S extends Schema | undefined> = S extends ({
[K: string]: Schema;
} ? ExtractObject<S> : never;

type OnlyOptional<S extends string> = S extends `${infer K}}?` ? K : S extends `${infer K}?` ? K : never;
type OnlyRequired<S extends string> = S extends `${string}?` ? never : S;
type OnlyOptional<S extends string> = S extends `${infer K}}` ? K : never;
type OnlyRequired<S extends string> = S extends `${string}}` ? never : S;
/** Parameters for a given path */
type PathArgs<S extends string> = PathKeys<S> extends never ? unknown : KeysToArgs<PathKeys<S>>;
/** Computes the union of keys for a path string */
type PathKeys<S extends string> = string extends S ? string : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? PathKeys<A> | PathKeys<B> : PathSplits<S>;
type PathSplits<S extends string> = S extends (`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`) ? PathSplits<`:${K}`> | PathSplits<R> : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never;
type PathKeys<S extends string> = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys<A> | PathKeys<B> : PathSplits<S>;
type PathSplits<S extends string> = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits<R> : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never;
type KeysToArgs<Key extends string> = {
[K in Key as OnlyOptional<K>]?: string | number;
} & (OnlyRequired<Key> extends never ? unknown : {
Expand Down Expand Up @@ -1537,7 +1537,7 @@ type MutateEndpoint<O extends {
*/
declare let RestEndpoint: RestEndpointConstructor;

declare function getUrlBase(path: string): PathFunction;
declare function getUrlBase(path: string): PathFunction<ParamData>;
declare function getUrlTokens(path: string): Set<string>;

type ResourceExtension<R extends {
Expand Down
9 changes: 8 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3213,7 +3213,7 @@ __metadata:
"@babel/runtime": "npm:^7.17.0"
"@data-client/endpoint": "npm:^0.14.12"
"@types/node": "npm:^22.0.0"
path-to-regexp: "npm:^6.3.0"
path-to-regexp: "npm:^8.1.0"
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -23022,6 +23022,13 @@ __metadata:
languageName: node
linkType: hard

"path-to-regexp@npm:^8.1.0":
version: 8.1.0
resolution: "path-to-regexp@npm:8.1.0"
checksum: 10c0/1c46be3806ab081bedc51eb238fcb026b61b15f19e8924b26e7dad88812dda499efe357a780665dc915dcab3be67213f145f5e2921b8fc8c6c497608d4e092ed
languageName: node
linkType: hard

"path-type@npm:^3.0.0":
version: 3.0.0
resolution: "path-type@npm:3.0.0"
Expand Down

0 comments on commit 0f9f55d

Please sign in to comment.