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
, beforeUpdate
, afterInsert
, and afterUpdate
functions, along with the shouldFetchFullDoc
option:
1const customPersistable = persistable({ 2 shouldFetchFullDoc: true, // When true, fetches the full document for updates 3 beforeInsert: ({ doc }) => { 4 // Modify the document before insertion 5 return { ...doc, customField: 'value' }; 6 }, 7 beforeUpdate: ({ doc }) => { 8 // Modify the document before update 9 return { ...doc, lastModified: new Date() }; 10 }, 11 afterInsert: ({ doc }) => { 12 // Any action after the document has been inserted 13 }, 14 afterUpdate: ({ doc, oldDoc }) => { 15 // Any action after the document has been updated 16 // When shouldFetchFullDoc is true, oldDoc contains the full previous document 17 }, 18}); 19 20export const CustomUsersCollection = createCollection({ 21 name: 'customUsers', 22 composers: [customPersistable], 23});
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 shouldFetchFullDoc: false, // Optional, defaults to false 7 })], 8});
Basic usage example:
1// Example of soft removal 2const user = await UsersCollection.insertAsync({ name: 'John Doe' }); 3await UsersCollection.removeAsync(user._id); 4 5// The user is not actually removed, but marked as removed (using option includeSoftRemoved) 6const removedUser = await UsersCollection.findOneAsync( 7 { _id: user._id }, 8 { includeSoftRemoved: true } 9); 10console.log(removedUser); // { _id: ..., name: 'John Doe', isRemoved: true } 11 12// The user seems to be removed in normal queries 13const removedUser2 = await UsersCollection.findOneAsync({ _id: user._id }); 14console.log(removedUser2); // null
You can customize the softRemoval
composer by providing the afterRemove
function and shouldFetchFullDoc
option:
1const customSoftRemoval = softRemoval({ 2 shouldFetchFullDoc: true, // When true, fetches full documents before removal 3 afterRemove: ({ docs, collection, isRemove, isHardRemove }) => { 4 // docs will contain the full documents when shouldFetchFullDoc is true 5 // isHardRemove indicates if it was a permanent removal 6 // Any action after the documents have been removed 7 }, 8}); 9 10export const CustomUsersCollection = createCollection({ 11 name: 'customUsers', 12 composers: [customSoftRemoval], 13}); 14 15// For hard removal: 16await CustomUsersCollection.removeAsync(user._id, { hardRemove: 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