Skip to content

Commit

Permalink
Boards collection
Browse files Browse the repository at this point in the history
  • Loading branch information
ovcharik committed Nov 5, 2014
1 parent 40c38c7 commit 548ae3c
Show file tree
Hide file tree
Showing 27 changed files with 517 additions and 29 deletions.
212 changes: 210 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

После конфигурации можем убедиться, что в токены авторизации сохранились.

Expand Down Expand Up @@ -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
Expand All @@ -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) утилиты, можно убедиться, что подобный подход будет работать оптимально. И тут важно понимать, что в нашем случае изменения в коллекции пользователей не будут синхронизироваться с клиентом, но так и задумывалось. Кстати для простого "джоина" можно просто возвращать массив на курсор.

Для отображения досок пользователей, я добавил новые подписки в роутеры и заверстал необходимые шаблоны, но все эти моменты мы уже рассмотрели выше, если вам интересны все изменения, то их можно увидеть здесь.
2 changes: 2 additions & 0 deletions todo-list/.meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ dburles:collection-helpers
jparker:gravatar
ovcharik:alertifyjs
reactive-var
aldeed:collection2
sergeyt:typeahead

Empty file removed todo-list/client/.gitkeep
Empty file.
Empty file.
15 changes: 15 additions & 0 deletions todo-list/client/components/board_card/board_card.jade
Original file line number Diff line number Diff line change
@@ -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

7 changes: 7 additions & 0 deletions todo-list/client/components/profile/profile.jade
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Empty file removed todo-list/client/config/.gitkeep
Empty file.
Empty file removed todo-list/client/layouts/.gitkeep
Empty file.
Empty file removed todo-list/client/lib/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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) ->
Expand Down
34 changes: 34 additions & 0 deletions todo-list/client/lib/1.base_profile_controller.coffee
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 3 additions & 0 deletions todo-list/client/lib/blaze.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Empty file removed todo-list/client/routes/.gitkeep
Empty file.
17 changes: 5 additions & 12 deletions todo-list/client/routes/home.coffee
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
# 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()

# рендерим шаблон профайла если пользователь авторизован
# и домашнюю страницу в противном случае
action: ->
if @isUserPresent()
@render 'profile'
else
super()
@render 'home'
Loading

0 comments on commit 548ae3c

Please sign in to comment.