Skip to content

Commit 3ccc905

Browse files
committed
urlToFields
We used to have urlToUserFields, with the idea of having a separate one for each type, but as you get into relationships and compound documents, it's more often the case where we want all of the sparse fieldset data for all types at once. This stores that data in a Fields interface, and establishes a urlToFields method that can be expanded to handle additional types.
1 parent d65a7e3 commit 3ccc905

16 files changed

+104
-88
lines changed

collections/users/controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import UserRepository from './repository.ts'
33
import sendJSON from '../../utils/send-json.ts'
44
import sendNoContent from '../../utils/send-no-content.ts'
55
import userToUserResponse from '../../utils/transformers/user-to/user-response.ts'
6-
import urlToUserFields from '../../utils/transformers/url-to/user-fields.ts'
6+
import urlToFields from '../../utils/transformers/url-to/fields.ts'
77

88
class UserController {
99
static get (ctx: Context, url?: URL) {
1010
const fieldSrc = url ?? ctx
11-
const fields = urlToUserFields(fieldSrc)
11+
const fields = urlToFields(fieldSrc)
1212
const res = userToUserResponse(ctx.state.user, fields)
1313
sendJSON(ctx, res)
1414
}
@@ -25,7 +25,7 @@ class UserController {
2525
await users.save(user)
2626

2727
const fieldSrc = url ?? ctx
28-
const fields = urlToUserFields(fieldSrc)
28+
const fields = urlToFields(fieldSrc)
2929
const res = userToUserResponse(user, fields)
3030
sendJSON(ctx, res)
3131
}

types/fields.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { type UserAttributesKeys } from './user-attributes.ts'
1+
import { type UserAttributesKeys, userAttributes } from './user-attributes.ts'
22

33
export default interface Fields {
4-
users: UserAttributesKeys[]
4+
users: readonly UserAttributesKeys[]
55
}
6+
7+
const createFields = (overrides?: Partial<Fields>): Fields => {
8+
const defaultFields: Fields = {
9+
users: userAttributes
10+
}
11+
12+
return { ...defaultFields, ...overrides }
13+
}
14+
15+
export { createFields }

types/user-attributes.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ export default interface UserAttributes {
66
username?: string
77
}
88

9-
const allUserAttributes = ['name', 'username'] as const
10-
const publicUserAttributes = ['name', 'username'] as const
11-
type UserAttributesKeys = (typeof allUserAttributes)[number]
9+
const userAttributes = ['name', 'username'] as const
10+
type UserAttributesKeys = (typeof userAttributes)[number]
1211

1312
const createUserAttributes = (overrides?: Partial<UserAttributes>): UserAttributes => {
1413
const defaultUserAttributes: UserAttributes = {
@@ -19,6 +18,7 @@ const createUserAttributes = (overrides?: Partial<UserAttributes>): UserAttribut
1918
return { ...defaultUserAttributes, ...overrides }
2019
}
2120

21+
// deno-lint-ignore no-explicit-any
2222
const isUserAttributes = (candidate: any): candidate is UserAttributes => {
2323
if (!isObject(candidate)) return false
2424
const obj = candidate as Record<string, unknown>
@@ -30,7 +30,6 @@ const isUserAttributes = (candidate: any): candidate is UserAttributes => {
3030
export {
3131
createUserAttributes,
3232
isUserAttributes,
33-
allUserAttributes,
34-
publicUserAttributes,
33+
userAttributes,
3534
type UserAttributesKeys
3635
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, it } from 'jsr:@std/testing/bdd'
2+
import { expect } from 'jsr:@std/expect'
3+
import { createUserAttributes, userAttributes } from '../../../types/user-attributes.ts'
4+
import getAllFieldCombinations from '../../testing/get-all-field-combinations.ts'
5+
import getRoot from '../../get-root.ts'
6+
import urlToFields from './fields.ts'
7+
8+
describe('urlToFields', () => {
9+
describe('User fields', () => {
10+
it('returns public attributes if there is no fields[users] parameter', () => {
11+
const url = new URL(`${getRoot()}/users`)
12+
const actual = urlToFields(url)
13+
expect(actual.users).toEqual(userAttributes)
14+
})
15+
16+
it('returns the fields specified', () => {
17+
const attributes = createUserAttributes()
18+
const objects = getAllFieldCombinations(attributes)
19+
for (const object of objects) {
20+
const fields = Object.keys(object)
21+
const url = new URL(`${getRoot()}/users?this=1&fields[users]=${fields.join(',')}&that=2`)
22+
const actual = urlToFields(url)
23+
expect(actual.users).toEqual(fields)
24+
}
25+
})
26+
})
27+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Context } from '@oak/oak'
2+
import { intersect } from '@std/collections'
3+
import Fields, { createFields } from '../../../types/fields.ts'
4+
5+
const urlToFields = (input: Context | URL): Fields => {
6+
const url = (input as Context)?.request?.url ?? input
7+
const fields = createFields()
8+
9+
const updateField = <K extends keyof Fields>(key: K) => {
10+
const param = url.searchParams.get(`fields[${key}]`)
11+
if (param !== null) {
12+
const requested = param.split(',').map(field => field.trim()) as Fields[K]
13+
fields[key] = intersect(requested, fields[key]) as Fields[K]
14+
}
15+
}
16+
17+
(Object.keys(fields) as (keyof Fields)[]).forEach(updateField)
18+
return fields
19+
}
20+
21+
export default urlToFields

utils/transformers/url-to/user-fields.test.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

utils/transformers/url-to/user-fields.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

utils/transformers/user-to/included-resource.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, it } from 'jsr:@std/testing/bdd'
22
import { expect } from 'jsr:@std/expect'
3+
import { createFields } from '../../../types/fields.ts'
34
import { createUser } from '../../../types/user.ts'
4-
import { type UserAttributesKeys, allUserAttributes, createUserAttributes } from '../../../types/user-attributes.ts'
5+
import { type UserAttributesKeys, userAttributes, createUserAttributes } from '../../../types/user-attributes.ts'
56
import getRoot from '../../get-root.ts'
67
import getAllFieldCombinations from '../../testing/get-all-field-combinations.ts'
78
import userToIncludedResource from './included-resource.ts'
@@ -29,13 +30,13 @@ describe('userToIncludedResource', () => {
2930
it('can return a sparse fieldset', () => {
3031
const objects = getAllFieldCombinations(attributes)
3132
for (const object of objects) {
32-
const fields = Object.keys(object) as UserAttributesKeys[]
33-
const excluded = allUserAttributes.filter(attr => !fields.includes(attr))
33+
const fields = createFields({ users: Object.keys(object) as UserAttributesKeys[] })
34+
const excluded = userAttributes.filter(attr => !fields.users.includes(attr))
3435
const actual = userToIncludedResource(user, fields)
3536
const attributes = actual.attributes!
3637

37-
expect(Object.keys(attributes)).toHaveLength(fields.length)
38-
for (const field of fields) expect(attributes[field]).toBe(user[field])
38+
expect(Object.keys(attributes)).toHaveLength(fields.users.length)
39+
for (const field of fields.users) expect(attributes[field]).toBe(user[field])
3940
for (const ex of excluded) expect(attributes[ex]).not.toBeDefined()
4041
}
4142
})

utils/transformers/user-to/included-resource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import type Fields from '../../../types/fields.ts'
12
import type User from '../../../types/user.ts'
23
import type UserResource from '../../../types/user-resource.ts'
3-
import { type UserAttributesKeys, publicUserAttributes } from '../../../types/user-attributes.ts'
44
import userToLink from './link.ts'
55
import userToUserAttributes from './user-attributes.ts'
66

7-
const userToIncludedResource = (user: User, fields: readonly UserAttributesKeys[] = publicUserAttributes): UserResource => {
7+
const userToIncludedResource = (user: User, fields?: Fields): UserResource => {
88
return {
99
links: {
1010
self: userToLink(user)

utils/transformers/user-to/user-attributes.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, it } from 'jsr:@std/testing/bdd'
22
import { expect } from 'jsr:@std/expect'
3+
import { createFields } from '../../../types/fields.ts'
34
import { createUser } from '../../../types/user.ts'
4-
import { type UserAttributesKeys, allUserAttributes, createUserAttributes } from '../../../types/user-attributes.ts'
5+
import { type UserAttributesKeys, userAttributes, createUserAttributes } from '../../../types/user-attributes.ts'
56
import getAllFieldCombinations from '../../testing/get-all-field-combinations.ts'
67
import userToUserAttributes from './user-attributes.ts'
78

@@ -19,12 +20,12 @@ describe('userToUserAttributes', () => {
1920
it('can return a sparse fieldset', () => {
2021
const objects = getAllFieldCombinations(attributes)
2122
for (const object of objects) {
22-
const fields = Object.keys(object) as UserAttributesKeys[]
23-
const excluded = allUserAttributes.filter(attr => !fields.includes(attr))
23+
const fields = createFields({ users: Object.keys(object) as UserAttributesKeys[] })
24+
const excluded = userAttributes.filter(attr => !fields.users.includes(attr))
2425
const actual = userToUserAttributes(user, fields)
2526

26-
expect(Object.keys(actual)).toHaveLength(fields.length)
27-
for (const field of fields) expect(actual[field]).toBe(user[field])
27+
expect(Object.keys(actual)).toHaveLength(fields.users.length)
28+
for (const field of fields.users) expect(actual[field]).toBe(user[field])
2829
for (const ex of excluded) expect(actual[ex]).not.toBeDefined()
2930
}
3031
})

0 commit comments

Comments
 (0)