Skip to content

feat: Add schema.Collection #2593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 26, 2023
Merged

feat: Add schema.Collection #2593

merged 1 commit into from
Apr 26, 2023

Conversation

ntucker
Copy link
Collaborator

@ntucker ntucker commented Apr 18, 2023

Fixes #2566, #1548, #2602, #2553, #2554, #428, #398, #358 .

Motivation

Atomic mutations are one of the core values of Rest Hooks. However, while update was in the first pre-release, and delete quickly added for version 1, create has always been a bit awkward.

Often creating a new element you are on a view where you want to see it added to a list somewhere.

One of the challenges here is that there can be many distinct lists you view. Furthermore, where something is added to a list isn't always the same.

Previously

https://resthooks.io/docs/concepts/atomic-mutations#create

const createUser = new Endpoint(postToUserFunction, {
  schema: User,
  update: (newUserId: string) => ({
    [userList.key()]: (users = []) => [newUserId, ...users],
  }),
});

Problems:

  • Very awkward code we force users to write
  • Users only had access to the pk, rather than full entity
  • Only works on top level results (doesn't work with nesting)
  • Forces users to enumerate all lists they might want to add - even if they aren't in store
    • Which means they may create a list that hasn't been populated

TODO

  • Test Values
  • Test addWith
  • Test pagination
  • Add simpler cursor name pagination
  • Test nested collections
  • Unions
  • .insert

Solution

Collections: Entities but for Arrays and Values instead of classes.

They literally are entities, so they get normalized as such. They just have different lifecycle implementations.

Inspiration: Backbone collections

Usage

class Todo extends IDEntity {
  userId = 0;
  title = '';
  completed = false;

  static key = 'Todo';
}

class User extends IDEntity {
  name = '';
  username = '';
  email = '';
  todos: Todo[] = [];

  static key = 'User';
  static schema = {
    todos: new schema.Collection([Todo], {
      nestKey: (parent, key) => ({
        userId: parent.id,
      }),
    }),
  };
}

const UserResource = createResource({
  path: '/users/:id',
  schema: User,
});
const TodoResource = createResource({
  path: '/todos/:id',
  searchParams: {} as { userId?: string } | undefined,
  schema: Todo,
});
const handleCreate = data => ctrl.fetch(TodoResource.getList.push, { userId: currentuser.id }, data);

This will add to these if they are already in store (but only if they already exist):

  • User({ id: currentUser.id }).todos
  • getTodo({ userId: currentUser.id })
  • getTodo()

Collection

construction

Takes Array or Values as first arg. Second arg is options object.

const userTodos = new schema.Collection(new schema.Array(Todo), {
  argsKey: ({ userId }: { userId: string }) => ({
    userId,
  }),
});

Options:

Must provide argsKey or nestKey to compute a serializable object to be used in PK table. argsKey takes ...args and nestKey takes (parent,key) as inputs.

createCollectionFilter is optional and can be used to override the default filter algorithm. This is used by the "adders" as they inherit from the collection.

const defaultFilter =
  (urlParams: Record<string, any>, body?: Record<string, any>) =>
  (collectionKey: Record<string, any>) =>
    Object.entries(collectionKey).every(
      ([key, value]) =>
        key.startsWith('order') ||
        urlParams[key] === value ||
        body?.[key] === value,
    );

addWith(merge, createCollectionFilter)

This is the general collection extender method. All specializations simply call this method. This is also used in the paginator as it has a custom algorithm.

merge overrides the Entity.merge() algorithm. This is used to determine where new values are placed (end, beginning, somewhere in the middle?)

createCollectionFilter is optional and will override the inherited function from the Collection itself.

push/unshift/assign

These all call addWith() with variations on merge to place newly created objects at the beginning or end respectively for Arrays. assign is for Values() and simply merges (there is no concept of order in Values)

RestEndpoint adder specializations

For a RestEndpoint containing a collection schema, the extenders push/unshift/assign can be used as convenience to create an appropriate endpoint. They use the parent schema, but us their respective addWith() variations. Additionally this sets the method to "POST".

Pagination

.paginated() now detects whether there is a collection or array. With collections it will use a custom extender. Arrays will work via update.

new .paginated('cursor') - 99% of paginations simply extract one field from the args. Let the user specify that field name rather than writing a whole function.

const handleLoadNextPage = () => ctrl.fetch(TodoResource.getList.paginated('cursor'), {cursor});

Normalizr changes

args are now part of both normalization and denormalization. Of course these will default to an empty array in case users don't need args. However, they will need to be passed on my implementations such as schema.normalize(). This shouldn't be a problem as schema implementations without it wont be expecting it anyway.

pk() now takes a fourth argument - args.

next/createResource

  • Uses collections instead of arrays
    • create uses getList.push
  • Added new options
    • searchParams
      • searchParams only applies to getList and create.
    • body
      • body only applies to update,create,partialUpdate
    • requestInit
    • getHeaders
    • getRequestInit
    • fetchResponse
    • parseResponse

@changeset-bot
Copy link

changeset-bot bot commented Apr 18, 2023

🦋 Changeset detected

Latest commit: 62d48bb

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Apr 18, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
rest-hooks ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 26, 2023 8:40pm

@@ -87,7 +89,7 @@
localCacheKey[pk] = INVALID;
} else {
if (typeof schema.denormalizeOnly === 'function') {
localCacheKey[pk] = schema.denormalizeOnly(entityCopy, unvisit);
localCacheKey[pk] = schema.denormalizeOnly(entityCopy, args, unvisit);

Check warning

Code scanning / CodeQL

Prototype-polluting assignment

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](1). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](2). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](3). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](4). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](5). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](6). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](7). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](8). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](9). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](10). This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](11).
@ntucker ntucker force-pushed the collections branch 3 times, most recently from f190842 to dcda13e Compare April 20, 2023 17:37
@ntucker ntucker force-pushed the collections branch 2 times, most recently from 8bbe78e to 6c0f8ae Compare April 20, 2023 20:29
@codecov
Copy link

codecov bot commented Apr 20, 2023

Codecov Report

Patch coverage: 97.93% and project coverage change: -0.08 ⚠️

Comparison is base (950a982) 98.06% compared to head (62d48bb) 97.98%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2593      +/-   ##
==========================================
- Coverage   98.06%   97.98%   -0.08%     
==========================================
  Files         131      135       +4     
  Lines        2165     2287     +122     
  Branches      328      357      +29     
==========================================
+ Hits         2123     2241     +118     
  Misses         19       19              
- Partials       23       27       +4     
Impacted Files Coverage Δ
packages/core/src/controller/BaseController.ts 95.37% <ø> (ø)
packages/core/src/state/reducer/setReducer.ts 94.11% <ø> (ø)
packages/endpoint/src/schemas/Entity.ts 96.92% <ø> (ø)
packages/endpoint/src/schemas/Union.ts 100.00% <ø> (ø)
packages/endpoint/src/schemas/Values.ts 100.00% <ø> (ø)
packages/normalizr/src/schemas/ImmutableUtils.ts 92.85% <ø> (ø)
packages/normalizr/src/denormalize/globalCache.ts 98.07% <66.66%> (-1.93%) ⬇️
packages/endpoint/src/schemas/Collection.ts 95.16% <96.61%> (ø)
packages/endpoint/src/queryEndpoint.ts 100.00% <100.00%> (ø)
packages/endpoint/src/schemas/All.ts 100.00% <100.00%> (ø)
... and 15 more

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@ntucker ntucker force-pushed the collections branch 2 times, most recently from f623b85 to 2146759 Compare April 25, 2023 21:42
@ntucker
Copy link
Collaborator Author

ntucker commented Apr 26, 2023

Will update with insertions in a follow up PR

@ntucker ntucker merged commit dfcad22 into master Apr 26, 2023
@ntucker ntucker deleted the collections branch April 26, 2023 20:51
ntucker added a commit that referenced this pull request Apr 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant