quave:collections

v3.1.0Published 2 weeks ago

quave:collections

quave:collections is a Meteor package that allows you to create your collections in a standard way.

Features

  • Schemas
  • Helpers
  • Composers

Compatible with Meteor 3.0.3+

Why

Every application that connects to databases usually need the following features:

  • A way to access object instances when they come from the database: helpers
  • Provide new methods to collections: collection
  • Valid the data before persisting: schemas
  • Centralize behaviors: composers

Meteor has packages for almost all these use cases but it's not easy to find the best in each case and also how to use them together, that is why we have created this package.

We offer here a standard way for you to create your collections by configuring all these features in a function call createCollection using a bunch of options in a declarative way and without using Javascript classes.

We also allow you to extend your Meteor.users collection in the same way as any other collection.

We believe we are not reinventing the wheel in this package but what we are doing is like putting together the wheels in the vehicle :).

Installation

meteor add quave:collections
meteor npm install simpl-schema

Usage

Methods

Example applying collection property:

1import { createCollection } from 'meteor/quave:collections';
2
3export const AddressesCollection = createCollection({
4  name: 'addresses',
5  collection: {
6    save(addressParam) {
7      const address = { ...addressParam };
8
9      if (address._id) {
10        this.update(address._id, { $set: { ...address } });
11        return address._id;
12      }
13      delete address._id;
14      return this.insert({ ...address });
15    },
16  },
17});

Schema

Example applying SimpleSchema:

1import { createCollection } from 'meteor/quave:collections';
2
3import SimpleSchema from 'simpl-schema';
4
5const PlayerSchema = new SimpleSchema({
6  name: {
7    type: String,
8  },
9  age: {
10    type: SimpleSchema.Integer,
11  },
12});
13
14export const PlayersCollection = createCollection({
15  name: 'players',
16  schema: PlayerSchema,
17});

Composers

Built-in composers

persistable

The persistable composer adds a save method to your collection, which handles both inserting new documents and updating existing ones. It also automatically manages createdAt and updatedAt fields.

To use the persistable composer:

1import { createCollection, persistable } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  name: 'users',
5  composers: [persistable()],
6});

The save method can be used as follows:

1// Insert a new document
2const newUser = await UsersCollection.save({
3  name: 'John Doe',
4  email: 'john@example.com',
5});
6
7// Update an existing document
8const updatedUser = await UsersCollection.save({
9  _id: newUser._id,
10  name: 'John Updated',
11});
12
13// Save with custom selector to find existing document
14const user = await UsersCollection.save(
15  { email: 'john@example.com', name: 'John Doe' },
16  { selectorToFindId: { email: 'john@example.com' } }
17);
18
19// Save without returning the document
20await UsersCollection.save({ name: 'Alice' }, { skipReturn: true });
21
22// Save and return only specific fields
23const savedUser = await UsersCollection.save(
24  { name: 'Bob' },
25  { projection: { name: 1 } }
26);

You can customize the persistable composer by providing beforeInsert and beforeUpdate functions:

1const customPersistable = persistable({
2  beforeInsert: ({ doc }) => {
3    // Modify the document before insertion
4    return { ...doc, customField: 'value' };
5  },
6  beforeUpdate: ({ doc }) => {
7    // Modify the document before update
8    return { ...doc, lastModified: new Date() };
9  },
10});
11
12export const CustomUsersCollection = createCollection({
13  name: 'customUsers',
14  composers: [customPersistable],
15});

You can also customize the persistable composer by providing the afterInsert and afterUpdate functions, which apply an action after the document has been inserted or updated.

1const customPersistable = persistable({
2  afterInsert: ({ doc }) => {
3    // Any action after the document has been inserted
4  },
5  afterUpdate: ({ doc }) => {
6    // Any action after the document has been updated
7  },
8});
9
10export const CustomUsersCollection = createCollection({
11  name: 'customUsers',
12  composers: [customPersistable],
13});

Also, you can use the shouldFetchFullDoc flag to decide whether the old document should be fetched from the database and used in the afterUpdate function.

1const savedUser = await UsersCollection.save(
2  { _id: user._id, name: 'Bob' },
3  { shouldFetchFullDoc: true }
4);

The persistable composer provides a convenient way to handle document persistence with automatic timestamp management and customizable pre-save hooks.

softRemoval

The softRemoval composer adds soft deletion functionality to your collection. Instead of permanently deleting documents, it marks them as removed and filters them out of normal queries.

To use the softRemoval composer:

1import { createCollection, softRemoval } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  name: 'users',
5  composers: [softRemoval()],
6});
7
8// Example of soft removal
9const user = await UsersCollection.insertAsync({ name: 'John Doe' });
10await UsersCollection.removeAsync(user._id);
11
12// The user is not actually removed, but marked as removed (using option includeSoftRemoved)
13const removedUser = await UsersCollection.findOneAsync(
14  { _id: user._id },
15  { includeSoftRemoved: true }
16);
17console.log(removedUser); // { _id: ..., name: 'John Doe', isRemoved: true }
18
19// The user seems to be removed
20const removedUser2 = await UsersCollection.findOneAsync({ _id: user._id });
21console.log(removedUser2); // null

You can also customize the softRemoval composer by providing the afterRemove function. This function will be called after the documents has been removed, and you can provide the documents to this function by adding the flag shouldFetchFullDoc.

1const customSoftRemoval = softRemoval({
2  afterRemove: ({ docs }) => {
3    // Any action after the documents have been removed
4  },
5});
1await UsersCollection.removeAsync(user._id, { shouldFetchFullDoc: true });

Create your own composer

Example creating a way to paginate the fetch of data using composers

1import { createCollection } from 'meteor/quave:collections';
2
3const LIMIT = 7;
4export const paginable = (collection) =>
5  Object.assign({}, collection, {
6    getPaginated({ selector, options = {}, paginationAction }) {
7      const { skip, limit } = paginationAction || { skip: 0, limit: LIMIT };
8      const items = this.find(selector, {
9        ...options,
10        skip,
11        limit,
12      }).fetch();
13      const total = this.find(selector).count();
14      const nextSkip = skip + limit;
15      const previousSkip = skip - limit;
16
17      return {
18        items,
19        pagination: {
20          total,
21          totalPages: parseInt(total / limit, 10) + (total % limit > 0 ? 1 : 0),
22          currentPage:
23            parseInt(skip / limit, 10) + (skip % limit > 0 ? 1 : 0) + 1,
24          ...(nextSkip < total ? { next: { skip: nextSkip, limit } } : {}),
25          ...(previousSkip >= 0
26            ? { previous: { skip: previousSkip, limit } }
27            : {}),
28        },
29      };
30    },
31  });
32
33export const StoresCollection = createCollection({
34  name: 'stores',
35  composers: [paginable],
36});
37
38// This probably will come from the client, using Methods, REST, or GraphQL
39// const paginationAction = {skip: XXX, limit: YYY};
40
41const { items, pagination } = StoresCollection.getPaginated({
42  selector: {
43    ...(search ? { name: { $regex: search, $options: 'i' } } : {}),
44  },
45  options: { sort: { updatedAt: -1 } },
46  paginationAction,
47});

Options

Second argument for the default collections constructor. Example defining a transform function.

1const transform = (doc) => ({
2  ...doc,
3  get user() {
4    return Meteor.users.findOne(this.userId);
5  },
6});
7
8export const PlayersCollection = createCollection({
9  name: 'players',
10  schema,
11  options: {
12    transform,
13  },
14});

Meteor.users

Extending Meteor.users, also using collection, helpers, composers, apply.

You can use all these options also with name instead of instance.

1import { createCollection } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  instance: Meteor.users,
5  schema: UserTypeDef,
6  collection: {
7    isAdmin(userId) {
8      const user = userId && this.findOne(userId, { fields: { profiles: 1 } });
9      return (
10        user && user.profiles && user.profiles.includes(UserProfile.ADMIN.name)
11      );
12    },
13  },
14  helpers: {
15    toPaymentGatewayJson() {
16      return {
17        country: 'us',
18        external_id: this._id,
19        name: this.name,
20        type: 'individual',
21        email: this.email,
22      };
23    },
24  },
25  composers: [paginable],
26  apply(coll) {
27    coll.after.insert(userAfterInsert(coll), { fetchPrevious: false });
28    coll.after.update(userAfterUpdate);
29  },
30});

Publishing

Bump the version following semver in package.js and run meteor npm run generate-dts to generate the types.

Then publish the package:

meteor publish

License

MIT