diff --git a/README.md b/README.md index ce3387e..919d03e 100644 --- a/README.md +++ b/README.md @@ -348,8 +348,8 @@ template(name='about') ![base_auth_form](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/base_auth_form.png) +* [Результат](http://hgsm-base-user-auth.meteor.com/) * [Репозиторий](https://github.com/ovcharik/meteor-getting-started/tree/78c28cce3af54989ca8c89c453e37c578e8f1d52/todo-list) -* [Рабочая версия](http://hgsm-base-user-auth.meteor.com/) После конфигурации можем убедиться, что в токены авторизации сохранились. @@ -977,7 +977,7 @@ Template.profile.helpers defaultValue: @getPublicEmail() placeholder: 'Public email' scope: 'user' - path: 'prfile.email' + path: 'profile.email' icon: 'envelope' Template.profile.events @@ -1000,3 +1000,211 @@ Template.profile.events * [Репозитарий](https://github.com/ovcharik/meteor-getting-started/tree/2110ed5168155893fbaf27a29df0675070765d81/todo-list) Если вам что-то не понятно до текущего момента, то советую ознакомится с текущим состоянием проекта в [репозитарии](https://github.com/ovcharik/meteor-getting-started/tree/b1067c219e591de6d6eb387d10a107cfff180e69/todo-list), я старался комментировать в файлах все происходящее, также возможно стоит еще раз полистать написанное выше, может не совсем последовательно, но я старался уделить внимание всем ключевым моментам, и конечно же можно склонировать проект, на данном этапе и пощупать его руками. Дальше я собираюсь затронуть еще несколько тем: как создавать свои собственные коллекции, как можно защищать данные в коллекциях от нежелательного редактирования, расскажу немного про использование RPC и использование библиотек `npm` на сервере. + +## Еще про коллекции и подписки + +Прежде чем приступим к созданию своих коллекций предлагаю создать механизм, который будет автоматически вычислять некоторые поля при вставки/изменении данных в бд. Для этого добавим пакет [aldeed:collection2](https://github.com/aldeed/meteor-collection2), в который входит [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema). Данные пакеты позволят нам легко валидировать данные, добавлять индексы к коллекции и прочее. + +Я привык создавать части приложения с минимальным функционалом, а какие-то сложные вещи уже комбинировать из них. Поэтому добавим к пакету `aldeed:simple-schema` немного новых возможностей. + +```coffeescipt +# lib/simple_schema.coffee +_.extend SimpleSchema, + + # Данный метод будет из нескольких переданных объектов + # собирать одну схему и возвращать ее + build: (objects...) -> + result = {} + for obj in objects + _.extend result, obj + return new SimpleSchema result + + # Если добавить к схеме данный объект, + # то у модели появится два поля которые будут автоматически + # вычисляться + timestamp: + createdAt: + type: Date + denyUpdate: true + autoValue: -> + if @isInsert + return new Date + if @isUpsert + return { $setOnInsert: new Date } + @unset() + + updatedAt: + type: Date + autoValue: -> + new Date +``` + +И создадим новую коллекцию + +```coffeescript +# collections/boards.coffee +# схема данных +boardsSchema = SimpleSchema.build SimpleSchema.timestamp, + 'name': + type: String + index: true + + 'description': + type: String + optional: true # не обязательное поле + + # автоматически генерируем автора доски + 'owner': + type: String + autoValue: (doc) -> + if @isInsert + return @userId + if @isUpsert + return { $setOnInsert: @userId } + @unset() + + # список пользователей доски + 'users': + type: [String] + defaultValue: [] + + 'users.$': + type: String + regEx: SimpleSchema.RegEx.Id + + +# регистрируем коллекцию и добавляем схему +Boards = new Meteor.Collection 'boards' +Boards.attachSchema boardsSchema + + +# защита данных +Boards.allow + # создавать доски может любой авторизованный пользователь + insert: (userId, doc) -> + userId && true + # обновлять данные может только создатель доски + update: (userId, doc) -> + userId && userId == doc.owner + + +# статические методы +_.extend Boards, + findByUser: (userId = Meteor.userId(), options) -> + Boards.find + $or: [ + { users: userId } + { owner: userId } + ] + , options + + create: (data, cb) -> + Boards.insert data, cb + +# методы объектов +Boards.helpers + update: (data, cb) -> + Boards.update @_id, data, cb + + addUser: (user, cb) -> + user = user._id if _.isObject(user) + @update + $addToSet: + users: user + , cb + + removeUser: (user, cb) -> + user = user._id if _.isObject(user) + @update + $pop: + users: user + , cb + + updateName: (name, cb) -> + @update { $set: {name: name} }, cb + + updateDescription: (desc, cb) -> + @update { $set: {description: desc} }, cb + + # joins + getOwner: -> + UsersCollection.findOne @owner + + getUsers: (options) -> + UsersCollection.find + $or: [ + { _id: @owner } + { _id: { $in: @users } } + ] + , options + + urlData: -> + id: @_id + + +# экспорт +@BoardsCollection = Boards +``` + +Первым делом при создании коллекции мы определили схему, это позволит нам валидировать данные и автоматически вычислять некоторые поля. Подробнее о валидации можно почитать на странице пакета [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema), там достаточно богатый функционал, и даже при установки дополнительного пакета `aldeed:autoform`, можно генерировать формы, которые сразу же будут оповещать об ошибках, при создании записи. + +Новую коллекцию в бд мы создаем вызовом `Boards = new Meteor.Collection 'boards'`, если ее нет, либо подключаемся к существующей. В принципе это весь необходимый функционал для создания новых коллекций, там есть [еще пара](https://docs.meteor.com/#/full/mongo_collection) опций, которые можно указать при создании. + +С помощью метода `allow` у коллекции мы можем контролировать доступ к изменению данных в коллекции. В текущем примере мы запрещаем создавать новые записи в коллекции для всех неавторизованных пользователей, и разрешаем изменять данные только для создателя доски. Эти проверки будут осуществляться на сервере и можно не переживать, что какой-нибудь кулцхакер поменяет эту логику на клиенте. Также в вашем распоряжении есть практически аналогичный метод `deny`, думаю суть его ясна. Подробнее про [allow](https://docs.meteor.com/#/full/allow). + +При выводе карточки доски я хочу сразу отображать данные о создателе доски. Но если мы подпишемся только на доски, то эти данные поступать на клиент не будут. Однако публикации в метеоре дают возможность подписки на любые данные, даже автоматически вычисляемые, типа счетчиков коллекций и прочего. + +```coffeescript +# server/publications/boards.coffee +Meteor.publish 'boards', (userId, limit = 20) -> + findOptions = + limit: limit + sort: { createdAt: -1 } + + if userId + # доски конкретного пользователя + cursor = BoardsCollection.findByUser userId, findOptions + else + # все доски + cursor = BoardsCollection.find {}, findOptions + + inited = false + userFindOptions = + fields: + service: 1 + username: 1 + profile: 1 + + # колбек для добавления создателя доски к подписке + addUser = (id, fields) => + if inited + userId = fields.owner + @added 'users', userId, UsersCollection.findOne(userId, userFindOptions) + + # отслеживаем изменения в коллекции, + # что бы добавлять пользователей к подписке + handle = cursor.observeChanges + added: addUser + changed: addUser + + inited = true + # при инициализации сразу же добавляем пользователей, + # при помощи одного запроса в бд + userIds = cursor.map (b) -> b.owner + UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) => + @added 'users', u._id, u + + # перестаем слушать курсор коллекции, при остановке подписки + @onStop -> + handle.stop() + + return cursor +``` + +Так как монга не умеет делать запросы через несколько коллекций и выдавать уже обработанные данные, как это происходит в реляционных бд, нам придется доставать данные о создателей досок при помощи еще одного запроса, да и так удобнее работать в рамках моделей данных. + +Первым делом в зависимости от запроса мы достаем из базы нужные доски, после этого нам необходимо еще одним запросом достать пользователей. Методы `added`, `changed` и `removed` в контексте публикации могут управлять данными передаваемыми на клиент. Если мы в публикации возвращаем курсор коллекции, то эти методы будут вызываться автоматически в зависимости от состояния коллекции, поэтому мы и возвращаем курсор, но дополнительно в самой публикации подписываемся на изменения данных в коллекции досок, и высылаем на клиент данные о пользователях по мере необходимости. + +С помощью логов соединения по веб-сокетам либо при помощи [данной](https://github.com/arunoda/meteor-ddp-analyzer) утилиты, можно убедиться, что подобный подход будет работать оптимально. И тут важно понимать, что в нашем случае изменения в коллекции пользователей не будут синхронизироваться с клиентом, но так и задумывалось. Кстати для простого "джоина" можно просто возвращать массив на курсор. + +Для отображения досок пользователей, я добавил новые подписки в роутеры и заверстал необходимые шаблоны, но все эти моменты мы уже рассмотрели выше, если вам интересны все изменения, то их можно увидеть здесь. diff --git a/todo-list/.meteor/packages b/todo-list/.meteor/packages index 2151013..7df9fee 100644 --- a/todo-list/.meteor/packages +++ b/todo-list/.meteor/packages @@ -20,4 +20,6 @@ dburles:collection-helpers jparker:gravatar ovcharik:alertifyjs reactive-var +aldeed:collection2 +sergeyt:typeahead diff --git a/todo-list/.meteor/versions b/todo-list/.meteor/versions index c80e599..4264b33 100644 --- a/todo-list/.meteor/versions +++ b/todo-list/.meteor/versions @@ -3,6 +3,8 @@ accounts-github@1.0.2 accounts-google@1.0.2 accounts-oauth@1.1.2 accounts-password@1.0.4 +aldeed:collection2@2.2.0 +aldeed:simple-schema@1.0.3 anti:i18n@0.4.3 application-configuration@1.0.3 autoupdate@1.1.3 @@ -57,6 +59,7 @@ minifiers@1.1.2 minimongo@1.0.5 mizzao:bootstrap-3@3.3.0 mobile-status-bar@1.0.1 +mongo-livedata@1.0.6 mongo@1.0.8 mquandalle:jade@0.2.8 npm-bcrypt@0.7.7 @@ -71,6 +74,7 @@ reactive-var@1.0.3 reload@1.1.1 retry@1.0.1 routepolicy@1.0.2 +sergeyt:typeahead@0.10.5_7 service-configuration@1.0.2 session@1.0.4 sha@1.0.1 diff --git a/todo-list/client/.gitkeep b/todo-list/client/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/components/.gitkeep b/todo-list/client/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/components/board_card/board_card.jade b/todo-list/client/components/board_card/board_card.jade new file mode 100644 index 0000000..330b4a2 --- /dev/null +++ b/todo-list/client/components/board_card/board_card.jade @@ -0,0 +1,15 @@ +//- client/components/board_card/board_card.jade +template(name='boardCard') + .panel.panel-default + .panel-heading.board-card-heading + +linkTo route='boards_show' data=urlData + = name + .panel-body.board-card-description + = description + .panel-footer.board-card-footer + +with getOwner + +userAvatar user=this size=20 + +linkTo route='users_show' data=urlData + | @ + = getUsername + diff --git a/todo-list/client/components/profile/profile.jade b/todo-list/client/components/profile/profile.jade index 6a12785..45c7fc9 100644 --- a/todo-list/client/components/profile/profile.jade +++ b/todo-list/client/components/profile/profile.jade @@ -20,3 +20,10 @@ template(name='profile') .profile-right-side h1 Boards + .row + +each boards + .col-xs-6.col-md-4.col-lg-3 + +boardCard + .row.row-bottom + .col-xs-12 + +nextPageButton name='boards' diff --git a/todo-list/client/config/.gitkeep b/todo-list/client/config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/layouts/.gitkeep b/todo-list/client/layouts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/lib/.gitkeep b/todo-list/client/lib/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/lib/pageable_route_controller.coffee b/todo-list/client/lib/0.pageable_route_controller.coffee similarity index 83% rename from todo-list/lib/pageable_route_controller.coffee rename to todo-list/client/lib/0.pageable_route_controller.coffee index 02b52fb..e851c15 100644 --- a/todo-list/lib/pageable_route_controller.coffee +++ b/todo-list/client/lib/0.pageable_route_controller.coffee @@ -1,4 +1,4 @@ -# lib/pageable_route_controller.coffee +# client/lib/pageable_route_controller.coffee varName = (inst, name = null) -> name = name && "_#{name}" || "" "#{inst.constructor.name}#{name}_limit" @@ -10,12 +10,12 @@ class @PagableRouteController extends RouteController # количество загружаемых данных limit: (name = null) -> - Session.get(varName(@), name) || @perPage + Session.get(varName(@, name)) || @perPage # следующая страница incLimit: (name = null, inc = null) -> inc ||= @perPage - Session.set varName(@, name), (@limit() + inc) + Session.set varName(@, name), (@limit(name) + inc) # сборс количества resetLimit: (name = null) -> diff --git a/todo-list/client/lib/1.base_profile_controller.coffee b/todo-list/client/lib/1.base_profile_controller.coffee new file mode 100644 index 0000000..54f2a0c --- /dev/null +++ b/todo-list/client/lib/1.base_profile_controller.coffee @@ -0,0 +1,34 @@ +# client/lib/base_profile_controller.coffee +class @BaseProfileController extends PagableRouteController + + # используем уже готовый шаблон + template: 'profile' + + currentUserId: -> + false + + # подписываемся на доски текущего пользователя + subscriptions: -> + if @currentUserId() + [ + @subscribe 'user', @currentUserId() + @subscribe 'boards', @currentUserId(), @limit('boards') + ] + + # возвращаем данные о текущем пользователе, если такой имеется + data: -> + if @currentUserId() + { + user: UsersCollection.findOne @currentUserId() + boards: BoardsCollection.findByUser @currentUserId(), {sort: { createdAt: -1 }} + } + + loaded: (name) -> + switch name + when 'boards' then @limit('boards') > @data().boards.count() + else false + + onRun: -> + if @currentUserId() + @resetLimit('boards') + @next() diff --git a/todo-list/client/lib/blaze.coffee b/todo-list/client/lib/blaze.coffee index 6e81373..a1f9df4 100644 --- a/todo-list/client/lib/blaze.coffee +++ b/todo-list/client/lib/blaze.coffee @@ -6,5 +6,8 @@ helpers = nameFromPath: (base, path) -> ObjAndPath.valueFromPath(base, path) + isHomePage: -> + Router.current() instanceof HomeController + # добавляем хелперы в Blaze _(helpers).map (value, key) -> Blaze.registerHelper(key, value) diff --git a/todo-list/client/routes/.gitkeep b/todo-list/client/routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/routes/home.coffee b/todo-list/client/routes/home.coffee index 8958981..e536b6b 100644 --- a/todo-list/client/routes/home.coffee +++ b/todo-list/client/routes/home.coffee @@ -1,21 +1,14 @@ # client/routers/home.coffee Router.route '/', name: 'home' -class @HomeController extends PagableRouteController +class @HomeController extends BaseProfileController # авторизован ли пользователь? isUserPresent: -> !!Meteor.userId() - # подписываемся на профайл если пользователь авторизован - # на сайте - waitOn: -> - if @isUserPresent() - @subscribe 'profile' - - # возвращаем данные о текущем пользователе, если такой имеется - data: -> - if @isUserPresent() - { user: UsersCollection.findOne Meteor.userId() } + # ищем пользователя + currentUserId: -> + Meteor.userId() # рендерим шаблон профайла если пользователь авторизован # и домашнюю страницу в противном случае @@ -23,4 +16,4 @@ class @HomeController extends PagableRouteController if @isUserPresent() @render 'profile' else - super() + @render 'home' diff --git a/todo-list/client/routes/users_show.coffee b/todo-list/client/routes/users_show.coffee index 5a23a3d..5e7eea9 100644 --- a/todo-list/client/routes/users_show.coffee +++ b/todo-list/client/routes/users_show.coffee @@ -1,14 +1,6 @@ # client/routers/user_show.coffee Router.route '/users/:id', name: 'users_show' -class @UsersShowController extends PagableRouteController +class @UsersShowController extends BaseProfileController - # используем уже готовый шаблон - template: 'profile' - - # подписываемся на нужного пользователя - waitOn: -> - @subscribe 'user', @params.id - - # ищем нужного пользователя - data: -> - user: UsersCollection.findOneUser(@params.id) + currentUserId: -> + @params.id diff --git a/todo-list/client/styles/.gitkeep b/todo-list/client/styles/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/client/styles/main.less b/todo-list/client/styles/main.less index c597e73..9a4a18e 100644 --- a/todo-list/client/styles/main.less +++ b/todo-list/client/styles/main.less @@ -52,3 +52,48 @@ body { .row-bottom { margin-bottom: 15px; } + +.board-card-heading, +.board-card-footer { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.board-card-description { + min-height: 80px; + max-height: 80px; + overflow-y: auto; + overflow-x: hidden +} + + +.user-autocomplete-item { + padding: 5px 10px; + min-width: 200px; +} + +.user-autocomplete-avatar { + float: left; + width: 40px; +} + +.user-autocomplete-info { + margin-left: 50px; +} + +.user-autocomplete-name { + font-weight: bold; +} + +.form-users-avatar { + padding: 5px; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } +} diff --git a/todo-list/collections/.gitkeep b/todo-list/collections/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/collections/boards.coffee b/todo-list/collections/boards.coffee new file mode 100644 index 0000000..abfc50d --- /dev/null +++ b/todo-list/collections/boards.coffee @@ -0,0 +1,102 @@ +# collections/boards.coffee +# схема данных +boardsSchema = SimpleSchema.build SimpleSchema.timestamp, + 'name': + type: String + index: true + + 'description': + type: String + optional: true # не обязательное поле + + # автоматически генерируем автора доски + 'owner': + type: String + autoValue: (doc) -> + if @isInsert + return @userId + if @isUpsert + return { $setOnInsert: @userId } + @unset() + + # список пользователей доски + 'users': + type: [String] + defaultValue: [] + + 'users.$': + type: String + regEx: SimpleSchema.RegEx.Id + + +# регистрируем коллекцию и добавляем схему +Boards = new Meteor.Collection 'boards' +Boards.attachSchema boardsSchema + + +# защита данных +Boards.allow + # создавать доски может любой авторизованный пользователь + insert: (userId, doc) -> + userId && true + # обновлять данные может только создатель доски + update: (userId, doc) -> + userId && userId == doc.owner + + +# статические методы +_.extend Boards, + findByUser: (userId = Meteor.userId(), options) -> + Boards.find + $or: [ + { users: userId } + { owner: userId } + ] + , options + + create: (data, cb) -> + Boards.insert data, cb + +# методы объектов +Boards.helpers + update: (data, cb) -> + Boards.update @_id, data, cb + + addUser: (user, cb) -> + user = user._id if _.isObject(user) + @update + $addToSet: + users: user + , cb + + removeUser: (user, cb) -> + user = user._id if _.isObject(user) + @update + $pop: + users: user + , cb + + updateName: (name, cb) -> + @update { $set: {name: name} }, cb + + updateDescription: (desc, cb) -> + @update { $set: {description: desc} }, cb + + # joins + getOwner: -> + UsersCollection.findOne @owner + + getUsers: (options) -> + UsersCollection.find + $or: [ + { _id: @owner } + { _id: { $in: @users } } + ] + , options + + urlData: -> + id: @_id + + +# экспорт +@BoardsCollection = Boards diff --git a/todo-list/collections/users.coffee b/todo-list/collections/users.coffee index 25bde9e..4e70ab0 100644 --- a/todo-list/collections/users.coffee +++ b/todo-list/collections/users.coffee @@ -12,6 +12,16 @@ _.extend Users, findOneUser: (id, options) -> Users.findOne { $or: [ { _id: id }, { username: id } ] }, options + findAutocomplete: (q, options) -> + rg = new RegExp q, 'i' + Users.find + $or: [ + { '_id' : rg } + { 'username' : rg } + { 'profile.name' : rg } + ] + , options + # настройка коллекции Users.allow # разрешаем обновлять только указанные поля @@ -69,7 +79,7 @@ Users.helpers getPublicEmail : -> @profile?.email urlData: -> - id: @getUsername() + id: @_id # вычисляем ссылку на граватар, на основе адреса почты # или хеша автоматически вычисленного при регистрации diff --git a/todo-list/lib/.gitkeep b/todo-list/lib/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/lib/simple_schema.coffee b/todo-list/lib/simple_schema.coffee new file mode 100644 index 0000000..887cf9c --- /dev/null +++ b/todo-list/lib/simple_schema.coffee @@ -0,0 +1,29 @@ +# lib/simple_schema.coffee +_.extend SimpleSchema, + + # Данный метод будет из нескольких переданных объектов + # собирать одну схему и возвращать ее + build: (objects...) -> + result = {} + for obj in objects + _.extend result, obj + return new SimpleSchema result + + # Если добавить к схеме данный объект, + # то у модели появится два поля которые будут автоматически + # вычисляться + timestamp: + createdAt: + type: Date + denyUpdate: true + autoValue: -> + if @isInsert + return new Date + if @isUpsert + return { $setOnInsert: new Date } + @unset() + + updatedAt: + type: Date + autoValue: -> + new Date diff --git a/todo-list/server/.gitkeep b/todo-list/server/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/server/methods/.gitkeep b/todo-list/server/methods/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/server/publications/.gitkeep b/todo-list/server/publications/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/todo-list/server/publications/boards.coffee b/todo-list/server/publications/boards.coffee new file mode 100644 index 0000000..0492972 --- /dev/null +++ b/todo-list/server/publications/boards.coffee @@ -0,0 +1,44 @@ +# server/publications/boards.coffee +Meteor.publish 'boards', (userId, limit = 20) -> + findOptions = + limit: limit + sort: { createdAt: -1 } + + if userId + # доски конкретного пользователя + cursor = BoardsCollection.findByUser userId, findOptions + else + # все доски + cursor = BoardsCollection.find {}, findOptions + + inited = false + userFindOptions = + fields: + service: 1 + username: 1 + profile: 1 + + # колбек для добавления создателя доски к подписке + addUser = (id, fields) => + if inited + userId = fields.owner + @added 'users', userId, UsersCollection.findOne(userId, userFindOptions) + + # отслеживаем изменения в коллекции, + # что бы добавлять пользователей к подписке + handle = cursor.observeChanges + added: addUser + changed: addUser + + inited = true + # при инициализации сразу же добавляем пользователей, + # при помощи одного запроса в бд + userIds = cursor.map (b) -> b.owner + UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) => + @added 'users', u._id, u + + # перестаем слушать курсор коллекции, при остановке подписки + @onStop -> + handle.stop() + + return cursor