Skip to content

Commit

Permalink
fix(client): Replace placeholders in URL with route params (#3270)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdartic authored Oct 11, 2023
1 parent c59e1b8 commit a0624eb
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 17 deletions.
52 changes: 52 additions & 0 deletions docs/api/client/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,55 @@ curl -H "Content-Type: application/json" -H "X-Service-Method: myCustomMethod" -
```

This will call `messages.myCustomMethod({ message: 'Hello world' }, {})`.

### Route placeholders

Service URLs can have placeholders, e.g. `users/:userId/messages`. (see in [express](../express.md#params.route) or [koa](../koa.md#params.route))

You can call the client with route placeholders in the `params.route` property:

```ts
import { feathers } from '@feathersjs/feathers'
import rest from '@feathersjs/rest-client'

const app = feathers()

// Connect to the same as the browser URL (only in the browser)
const restClient = rest()

// Connect to a different URL
const restClient = rest('http://feathers-api.com')

// Configure an AJAX library (see below) with that client
app.configure(restClient.fetch(window.fetch.bind(window)))

// Connect to the `http://feathers-api.com/messages` service
const messages = app.service('users/:userId/messages')

// Call the `http://feathers-api.com/users/2/messages` URL
messages.find({
route: {
userId: 2,
},
})
```

This can also be achieved by using the client bundled,
sharing several `servicePath` variable exported in the [service shared file](`../../guides/cli/service.shared.md#Variables`) file.

```ts
import rest from '@feathersjs/rest-client'
// usersMessagesPath contains 'users/:userId/messages'
import { createClient, usersMessagesPath } from 'my-app'

const connection = rest('https://myapp.com').fetch(window.fetch.bind(window))

const client = createClient(connection)

// Call the `https://myapp.com/users/2/messages` URL
client.service(usersMessagesPath).find({
route: {
userId: 2
}
})
```
63 changes: 63 additions & 0 deletions docs/api/client/socketio.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,69 @@ Just like on the server _all_ methods you want to use have to be listed in the `

</BlockQuote>


### Route placeholders

Service URLs can have placeholders, e.g. `users/:userId/messages`. (see in [express](../express.md#params.route) or [koa](../koa.md#params.route))

You can call the client with route placeholders in the `params.route` property:

```ts
import { feathers } from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import io from 'socket.io-client'

const socket = io('http://api.feathersjs.com')
const app = feathers()

// Set up Socket.io client with the socket
app.configure(socketio(socket))

// Call `users/2/messages`
app.service('users/:userId/messages').find({
route: {
userId: 2
}
})
```

This can also be achieved by using the client bundled,
sharing several `servicePath` variable exported in the [service shared file](`../../guides/cli/service.shared.md#Variables`) file.

```ts
import rest from '@feathersjs/rest-client'

const connection = rest('https://myapp.com').fetch(window.fetch.bind(window))

const client = createClient(connection)

// Call the `https://myapp.com/users/2/messages` URL
client.service(usersMyMessagesPath).find({
route: {
userId: 2
}
})

import io from 'socket.io-client'
import socketio from '@feathersjs/socketio-client'
import { createClient, usersMessagesPath } from 'my-app'

const socket = io('http://api.my-feathers-server.com')
const connection = socketio(socket)

const client = createClient(connection)

const messageService = client.service('users/:userId/messages')

// Call `users/2/messages`
app.service('users/:userId/messages').find({
route: {
userId: 2
}
})
```


## Direct connection

Feathers sets up a normal Socket.io server that you can connect to with any Socket.io compatible client, usually the [Socket.io client](http://socket.io/docs/client-api/) either by loading the `socket.io-client` module or `/socket.io/socket.io.js` from the server. Query parameter types do not have to be converted from strings as they do for REST requests.
Expand Down
22 changes: 14 additions & 8 deletions packages/rest-client/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien
this.base = `${settings.base}/${this.name}`
}

makeUrl(query: Query, id?: string | number | null) {
makeUrl(query: Query, id?: string | number | null, route?: { [key: string]: string }) {
let url = this.base

if (route) {
Object.keys(route).forEach((key) => {
url = url.replace(`:${key}`, route[key])
})
}

query = query || {}

if (typeof id !== 'undefined' && id !== null) {
Expand Down Expand Up @@ -68,7 +74,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien
return this.request(
{
body: data,
url: this.makeUrl(params.query),
url: this.makeUrl(params.query, null, params.route),
method: 'POST',
headers: Object.assign(
{
Expand All @@ -92,7 +98,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien
_find(params?: P) {
return this.request(
{
url: this.makeUrl(params.query),
url: this.makeUrl(params.query, null, params.route),
method: 'GET',
headers: Object.assign({}, params.headers)
},
Expand All @@ -111,7 +117,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien

return this.request(
{
url: this.makeUrl(params.query, id),
url: this.makeUrl(params.query, id, params.route),
method: 'GET',
headers: Object.assign({}, params.headers)
},
Expand All @@ -126,7 +132,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien
_create(data: D, params?: P) {
return this.request(
{
url: this.makeUrl(params.query),
url: this.makeUrl(params.query, null, params.route),
body: data,
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers)
Expand All @@ -148,7 +154,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien

return this.request(
{
url: this.makeUrl(params.query, id),
url: this.makeUrl(params.query, id, params.route),
body: data,
method: 'PUT',
headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers)
Expand All @@ -170,7 +176,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien

return this.request(
{
url: this.makeUrl(params.query, id),
url: this.makeUrl(params.query, id, params.route),
body: data,
method: 'PATCH',
headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers)
Expand All @@ -192,7 +198,7 @@ export abstract class Base<T = any, D = Partial<T>, P extends Params = RestClien

return this.request(
{
url: this.makeUrl(params.query, id),
url: this.makeUrl(params.query, id, params.route),
method: 'DELETE',
headers: Object.assign({}, params.headers)
},
Expand Down
59 changes: 59 additions & 0 deletions packages/rest-client/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,63 @@ describe('REST client tests', function () {
message: 'Custom fetch client'
})
})

it('replace placeholder in route URLs', async () => {
const app = feathers()
let expectedValue: string | null = null
class MyFetchClient extends FetchClient {
request(options: any, _params: any) {
assert.equal(options.url, expectedValue)
return Promise.resolve()
}
}
app.configure(init('http://localhost:8889').fetch(fetch, {}, MyFetchClient))

expectedValue = 'http://localhost:8889/admin/todos'
await app.service(':slug/todos').find({
route: {
slug: 'admin'
}
})
await app.service(':slug/todos').create(
{},
{
route: {
slug: 'admin'
}
}
)
expectedValue = 'http://localhost:8889/admin/todos/0'
await app.service(':slug/todos').get(0, {
route: {
slug: 'admin'
}
})
expectedValue = 'http://localhost:8889/admin/todos/0'
await app.service(':slug/todos').patch(
0,
{},
{
route: {
slug: 'admin'
}
}
)
expectedValue = 'http://localhost:8889/admin/todos/0'
await app.service(':slug/todos').update(
0,
{},
{
route: {
slug: 'admin'
}
}
)
expectedValue = 'http://localhost:8889/admin/todos/0'
await app.service(':slug/todos').remove(0, {
route: {
slug: 'admin'
}
})
})
})
25 changes: 16 additions & 9 deletions packages/transport-commons/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,14 @@ export class Service<T = any, D = Partial<T>, P extends Params = Params>

send<X = any>(method: string, ...args: any[]) {
return new Promise<X>((resolve, reject) => {
args.unshift(method, this.path)
const route: Record<string, any> = args.pop()
let path = this.path
if (route) {
Object.keys(route).forEach((key) => {
path = path.replace(`:${key}`, route[key])
})
}
args.unshift(method, path)
args.push(function (error: any, data: any) {
return error ? reject(convert(error)) : resolve(data)
})
Expand All @@ -93,33 +100,33 @@ export class Service<T = any, D = Partial<T>, P extends Params = Params>
names.forEach((method) => {
const _method = `_${method}`
this[_method] = function (data: any, params: Params = {}) {
return this.send(method, data, params.query || {})
return this.send(method, data, params.query || {}, params.route || {})
}
this[method] = function (data: any, params: Params = {}) {
return this[_method](data, params)
return this[_method](data, params, params.route || {})
}
})
return this
}

_find(params: Params = {}) {
return this.send<T | T[]>('find', params.query || {})
return this.send<T | T[]>('find', params.query || {}, params.route || {})
}

find(params: Params = {}) {
return this._find(params)
}

_get(id: Id, params: Params = {}) {
return this.send<T>('get', id, params.query || {})
return this.send<T>('get', id, params.query || {}, params.route || {})
}

get(id: Id, params: Params = {}) {
return this._get(id, params)
}

_create(data: D, params: Params = {}) {
return this.send<T>('create', data, params.query || {})
return this.send<T>('create', data, params.query || {}, params.route || {})
}

create(data: D, params: Params = {}) {
Expand All @@ -130,23 +137,23 @@ export class Service<T = any, D = Partial<T>, P extends Params = Params>
if (typeof id === 'undefined') {
return Promise.reject(new Error("id for 'update' can not be undefined"))
}
return this.send<T>('update', id, data, params.query || {})
return this.send<T>('update', id, data, params.query || {}, params.route || {})
}

update(id: NullableId, data: D, params: Params = {}) {
return this._update(id, data, params)
}

_patch(id: NullableId, data: D, params: Params = {}) {
return this.send<T | T[]>('patch', id, data, params.query || {})
return this.send<T | T[]>('patch', id, data, params.query || {}, params.route || {})
}

patch(id: NullableId, data: D, params: Params = {}) {
return this._patch(id, data, params)
}

_remove(id: NullableId, params: Params = {}) {
return this.send<T | T[]>('remove', id, params.query || {})
return this.send<T | T[]>('remove', id, params.query || {}, params.route || {})
}

remove(id: NullableId, params: Params = {}) {
Expand Down
Loading

0 comments on commit a0624eb

Please sign in to comment.