├── README.md ├── docs ├── client.md ├── contribute.md ├── index.md ├── knex.md ├── methods.md ├── migrations.md ├── publish.md ├── relations.md └── transactions.md ├── examples ├── old │ └── pg-example │ │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ │ ├── migrations.jsx │ │ ├── packages │ │ ├── pg-example.css │ │ ├── pg-example.html │ │ └── pg-example.jsx ├── publish-relation │ ├── .knex │ │ ├── .gitignore │ │ ├── README │ │ ├── knexfile.js │ │ ├── migrations │ │ │ ├── 20150817153755_accounts.js │ │ │ └── 20150820112203_posts_comments.js │ │ └── package.json │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── index.html │ ├── index.js │ ├── run-app.sh │ └── tables.js └── react-todos │ ├── .knex │ ├── .gitignore │ ├── README │ ├── knexfile.js │ ├── migrations │ │ ├── 20150817153755_accounts.js │ │ └── 20150817154406_todos.js │ └── package.json │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ ├── README.md │ ├── client │ ├── components │ │ ├── AppBody.jsx │ │ ├── AppLoading.jsx │ │ ├── AppNotFound.jsx │ │ ├── ConnectionIssueDialog.jsx │ │ ├── LeftPanel.jsx │ │ ├── MenuOpenToggle.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoLists.jsx │ │ ├── accounts │ │ │ ├── AuthErrors.jsx │ │ │ ├── AuthFormInput.jsx │ │ │ ├── AuthJoinPage.jsx │ │ │ ├── AuthSignInPage.jsx │ │ │ └── UserSidebarSection.jsx │ │ └── todo-list │ │ │ ├── TodoListHeader.jsx │ │ │ ├── TodoListItems.jsx │ │ │ └── TodoListPage.jsx │ ├── index.html │ ├── lib │ │ ├── app.browserify.js │ │ ├── app.browserify.js.cached │ │ ├── app.browserify.js.map │ │ ├── app.browserify.options.json │ │ └── jquery.touchwipe.js │ ├── routes.jsx │ └── stylesheets │ │ ├── components │ │ ├── app-not-found.import.less │ │ ├── auth.import.less │ │ ├── lists-show.import.less │ │ └── loading.import.less │ │ ├── globals │ │ ├── base.import.less │ │ ├── button.import.less │ │ ├── form.import.less │ │ ├── icon.import.less │ │ ├── layout.import.less │ │ ├── link.import.less │ │ ├── list-items.import.less │ │ ├── menu.import.less │ │ ├── message.import.less │ │ ├── nav.import.less │ │ └── notification.import.less │ │ ├── main.less │ │ └── util │ │ ├── fontface.import.less │ │ ├── helpers.import.less │ │ ├── lesshat.import.less │ │ ├── reset.import.less │ │ ├── text.import.less │ │ ├── typography.import.less │ │ └── variables.import.less │ ├── lib │ ├── lists.js │ └── todos.js │ ├── mobile-config.js │ ├── packages.json │ ├── packages │ └── npm-container │ │ ├── .npm │ │ └── package │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ │ ├── index.js │ │ └── package.js │ ├── public │ ├── apple-touch-icon-precomposed.png │ ├── favicon.png │ ├── font │ │ ├── OpenSans-Light-webfont.eot │ │ ├── OpenSans-Light-webfont.svg │ │ ├── OpenSans-Light-webfont.ttf │ │ ├── OpenSans-Light-webfont.woff │ │ ├── OpenSans-Regular-webfont.eot │ │ ├── OpenSans-Regular-webfont.svg │ │ ├── OpenSans-Regular-webfont.ttf │ │ └── OpenSans-Regular-webfont.woff │ ├── icon │ │ ├── todos.eot │ │ ├── todos.svg │ │ ├── todos.ttf │ │ └── todos.woff │ └── img │ │ └── logo-todos.svg │ ├── resources │ ├── icons │ │ ├── icon-29x29.png │ │ ├── icon-29x29@2x.png │ │ ├── icon-36x36.png │ │ ├── icon-40x40.png │ │ ├── icon-40x40@2x.png │ │ ├── icon-48x48.png │ │ ├── icon-50x50.png │ │ ├── icon-50x50@2x.png │ │ ├── icon-57x57.png │ │ ├── icon-57x57@2x.png │ │ ├── icon-60x60.png │ │ ├── icon-60x60@2x.png │ │ ├── icon-72x72.png │ │ ├── icon-72x72@2x.png │ │ ├── icon-76x76.png │ │ ├── icon-76x76@2x.png │ │ └── icon-96x96.png │ └── splash │ │ ├── splash-1024x768.png │ │ ├── splash-1024x768@2x.png │ │ ├── splash-1280x720.png │ │ ├── splash-200x320.png │ │ ├── splash-320x200.png │ │ ├── splash-320x480.png │ │ ├── splash-320x480@2x.png │ │ ├── splash-320x568@2x.png │ │ ├── splash-480x320.png │ │ ├── splash-480x800.png │ │ ├── splash-720x1280.png │ │ ├── splash-768x1024.png │ │ ├── splash-768x1024@2x.png │ │ └── splash-800x480.png │ ├── run-app.sh │ └── server │ ├── bootstrap.js │ └── publish.js ├── img └── dumbo.jpg ├── mkdocs.yml ├── packages ├── accounts-base-pg-driver │ ├── README.md │ ├── accounts-base-pg-driver-tests.js │ ├── accounts-base-pg-driver.js │ └── package.js ├── accounts-base │ ├── .gitignore │ ├── README.md │ ├── accounts_client.js │ ├── accounts_common.js │ ├── accounts_rate_limit.js │ ├── accounts_server.js │ ├── accounts_tests.js │ ├── accounts_url_tests.js │ ├── globals_client.js │ ├── globals_server.js │ ├── localstorage_token.js │ ├── package.js │ ├── url_client.js │ └── url_server.js ├── accounts-password-pg-driver │ ├── README.md │ ├── accounts-password-pg-driver.js │ └── package.js ├── accounts-password │ ├── .gitignore │ ├── README.md │ ├── email_templates.js │ ├── package.js │ ├── password_client.js │ ├── password_server.js │ ├── password_tests.js │ └── password_tests_setup.js ├── accounts-ui-unstyled │ ├── .gitignore │ ├── README.md │ ├── accounts_ui.js │ ├── accounts_ui_tests.js │ ├── login_buttons.html │ ├── login_buttons.import.less │ ├── login_buttons.js │ ├── login_buttons_dialogs.html │ ├── login_buttons_dialogs.js │ ├── login_buttons_dropdown.html │ ├── login_buttons_dropdown.js │ ├── login_buttons_session.js │ ├── login_buttons_single.html │ ├── login_buttons_single.js │ └── package.js ├── bookshelf │ ├── .gitignore │ ├── .npm │ │ └── package │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ ├── README.md │ ├── bookshelf-tests.js │ ├── bookshelf.browserify.js │ ├── bookshelf.browserify.options.json │ ├── bookshelf.js │ ├── knex.js │ ├── knex_tests.js │ ├── package.js │ └── sync-promise.browserify.js └── pg │ ├── .npm │ └── package │ │ ├── .gitignore │ │ ├── README │ │ └── npm-shrinkwrap.json │ ├── README.md │ ├── collection-client.js │ ├── collection-server.js │ ├── collection.js │ ├── observe-driver │ ├── poll-n-diff.sql │ ├── poll.sql │ ├── polling-driver.js │ ├── setup-triggers.sql │ └── tests.js │ ├── package.js │ ├── pg-server-tests.js │ ├── pg-tests.js │ ├── pg.js │ ├── pre.js │ └── transaction.js ├── run-knex-client-tests.sh ├── run-observe-tests.sh └── run-tests.sh /README.md: -------------------------------------------------------------------------------- 1 | # Postgres Packages 2 | 3 | Elephants don't fly, most of the time. 4 | 5 | This is a heavily work in progress experimental exploration of what it would 6 | look like to use Postgres (and by extension, other SQL-like databases) with 7 | Meteor. Expect a lot of stuff to be broken for now until we announce that it's 8 | ready for usage. 9 | 10 | To see the patterns we are working on, check out the guide here: 11 | http://meteor-postgres.readthedocs.org/en/latest/ 12 | 13 | Tell use what you think on the [forum 14 | thread](https://forums.meteor.com/t/an-early-look-at-sql-in-meteor/8736)! 15 | 16 | ---------- 17 | 18 | ![Dumbo](/img/dumbo.jpg) 19 | -------------------------------------------------------------------------------- /docs/client.md: -------------------------------------------------------------------------------- 1 |

Client-side data cache

2 | 3 | In Meteor, the primary way of using data from your database is to "publish" it from the server, then "subscribe" to it from the client. This puts the data in a client-side version of your database table, which can be accessed with zero roundtrip time. 4 | 5 | Read more about [publishing data from SQL](publish.md). 6 | 7 | We have implemented a subset of the Knex query builder to work against Minimongo, Meteor's client-side data cache. With this technology, you can use the exact same query builder syntax to get data on the client and server. 8 | 9 | ```js 10 | // Declare a table 11 | Lists = new PG.Table("lists"); 12 | 13 | // Get the list with a specific ID 14 | Lists.where({ id: listId }).fetch()[0] 15 | ``` 16 | 17 | The above code will return the real row from the database if called on the server, or the client-side cached copy of the row if called from the client. 18 | 19 | ## Relations 20 | 21 | Read about how to use relations on the client here: [Relations](relations.md) 22 | 23 | ## Minimongo-compatible 24 | 25 | Currently, calling the Knex query builder on the client actually generates Minimongo queries, and the client-side data cache is Minimongo under the hood. This means it is compatible with any UI packages that rely on Minimongo or Minimongo cursors to function. 26 | 27 | For example, here is how you can observe a Knex query on the client: 28 | 29 | ```js 30 | Todos.where("list_id", 3).observe({ 31 | added(row) { 32 | console.log("added", row); 33 | } 34 | }); 35 | ``` 36 | 37 | Here is how you can use a Knex query in a Blaze helper: 38 | 39 | ```js 40 | Template.todos.helpers({ 41 | todos(listId) { 42 | // Note that you don't need to call fetch() since it works just like a Minimongo cursor 43 | return Todos.where("list_id", listId); 44 | } 45 | }) 46 | ``` 47 | 48 | ## _id field 49 | 50 | Rows you extract from the client-side cache will, in addition to all of the fields you expect, have an extra `_id` field which is a stringified version of the row's `id`. This is due to an internal implementation detail in DDP that requires every document to have a string `_id`. 51 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 |

Contributing to the project

2 | 3 | There's a lot more work left before SQL and Meteor is ready for general use. We could use your help to get it done faster! Thank you in advance for helping make this project a reality. 4 | 5 | ## Try it out 6 | 7 | It will be hard to figure out what improvements we should focus on without feedback. If you're an enterprising developer, try building some stuff with it and tell us what breaks. File issues [on GitHub](https://github.com/meteor/postgres-packages). 8 | 9 | ## Write docs 10 | 11 | If you find that something in the documentation isn't well explained or could use improvements, or if you discovered some new information and want to share it, please submit a PR to the [docs](https://github.com/meteor/postgres-packages/tree/master/docs). This is just as valuable as new code, since without good documentation nobody will be able to use this stuff. 12 | 13 | ## Write code 14 | 15 | ### Accounts 16 | 17 | Currently, this repository contains sketchy versions of Meteor's `accounts-base` and `accounts-password` packages that are modified to work with SQL. They have just enough functionality for super basic password login to work so that you can run the Todos example app, but there's a lot more to do here. 18 | 19 | In addition to building the functionality of the accounts packages, there is also some design to do about how a single package can work across different databases. 20 | 21 | See the [GitHub issues tagged `accounts`](https://github.com/meteor/postgres-packages/labels/accounts) for more details. 22 | 23 | ### Client-side cache 24 | 25 | Right now, there is a query builder that converts chained Knex queries into Minimongo queries, that lives [here](https://github.com/meteor/postgres-packages/blob/master/packages/bookshelf/knex.js). It could be improved to support more different kinds of queries. 26 | 27 | There are also other improvements that could be made. See more [on GitHub](https://github.com/meteor/postgres-packages/labels/client-cache). 28 | 29 | ### Relations and ORM 30 | 31 | Currently all of the support is build around the Knex query builder, but we haven't done that much about modeling relations, or supporting its ORM sibling, [Bookshelf](http://bookshelfjs.org/#). 32 | 33 | There's a [Quip document](https://quip.com/vsFjAQFIRdMs) about different ideas for publishing relational data from the server. It might be good to implement some of those ideas. 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Use PostgreSQL with Meteor 2 | 3 | This is the first preview of a possible implementation of full-stack SQL for Meteor. 4 | 5 | This is still a work in progress, and there is [a lot left to do](contribute.md), but we're really excited about what we have so far and wanted to share it. Some parts of the implementation are more reliable and stable than others, and we would love to have more help to add tests, detailed documentation, and new features. 6 | 7 | [Check out the code on GitHub.](https://github.com/meteor/postgres-packages) 8 | 9 | ## Concepts 10 | 11 | This is the simplest possible implementation of using a SQL database, specifically PostgreSQL, that we could come up with. The experience is not yet 100% seamless, and you have to put in some manual work for migrations, etc. Here are some core aspects to the design: 12 | 13 | 1. It is currently built around a monkey-patched and extended version of the excellent [Knex query builder library](http://knexjs.org/). 14 | 1. You can **publish any SQL query** from the server to the client using `Meteor.publish`. The data that ends up on the client is exactly the rows that are returned from the query. Read more here: [publishing data](publish.md) 15 | 1. You can only **run queries on the client** using the Knex query builder, which we have patched to enable specific query operators only. Read more here: [client-side queries](client.md) 16 | 1. For simple Meteor methods, you can **write your code once using Knex**, and you will get automatic optimistic UI. For complex methods, you will need to write separate code for the optimistic UI update, or just have your method run only on the server. Read more here: [methods](methods.md) 17 | 18 | ## Running the react-todos example app 19 | 20 | First, [install PostgreSQL](migrations.md#installing-and-running-postgresql). Then, run the commands below: 21 | 22 | ```bash 23 | # Clone this repository 24 | git clone https://github.com/meteor/postgres-packages.git 25 | 26 | # Set up Knex CLI tool 27 | cd postgres-packages/examples/react-todos/.knex/ 28 | npm install -g knex 29 | npm install 30 | 31 | # Create database and run migrations 32 | createdb todos 33 | knex migrate:latest 34 | 35 | # Run the app 36 | cd .. 37 | ./run-app.sh 38 | ``` 39 | 40 | ## Setting up an app startup script 41 | 42 | Our example apps each contain a script called `run-app.sh`, which sets up some of the environment variables you need. You will probably want to create your own script if you are starting a new app, with a different `POSTGRESQL_URL`. 43 | 44 | Here's the script explained: 45 | 46 | ```bash 47 | #! /bin/sh 48 | 49 | # Remind the user to set up the database 50 | echo "Make sure you have created a DB called 'todos', and that you have run the migrations in .knex/" 51 | 52 | # Set up environment variables: 53 | 54 | # URL for the database 55 | export POSTGRESQL_URL="postgres://127.0.0.1/todos" 56 | 57 | # Directory with cloned Postgres packages from this repo 58 | export PACKAGE_DIRS="$(dirname $0)/../../packages/" 59 | 60 | # Fake MONGO_URL so that Meteor doesn't start MongoDB for us 61 | export MONGO_URL="nope" 62 | 63 | # Go to the app's directory in case we ran the script from somewhere else 64 | cd "$(dirname $0)" 65 | 66 | # Run the app, and pass through any arguments passed to the script 67 | meteor "$@" 68 | ``` 69 | 70 | ## Acknowledgements 71 | 72 | Thank you to: 73 | 74 | 1. Ben Green for his Meteor PostgreSQL driver: [numtel/meteor-pg](https://github.com/numtel/meteor-pg) 75 | 2. Tim Griesser for the awesome Knex SQL query builder: [tgriesser/knex](https://github.com/tgriesser/knex) 76 | 3. The [Space Elephant](http://www.meteorpostgres.com/) team for their inspirational project and Devshop talk 77 | 78 | -------------------------------------------------------------------------------- /docs/methods.md: -------------------------------------------------------------------------------- 1 |

Writing to the database with methods

2 | 3 | In Meteor, [methods](http://docs.meteor.com/#/full/meteor_methods) are the main way to trigger persistent changes to data. 4 | 5 | ## Recap of optimistic UI 6 | 7 | As you may know, every method can be run on the client and the server to achieve optimistic UI. This means you can run a simulation on the client so that the UI seems fast, and then run the full method against the database to do the actual write. Meteor does all of the heavy lifting for you to make sure that the UI ends up representing the real change to the database, rolling back the simulation on the client if necessary. Read more here: [Optimistic UI with Meteor](http://info.meteor.com/blog/optimistic-ui-with-meteor-latency-compensation) 8 | 9 | If you define a method only on the server, no simulation will run, and it will act as a regular RPC. 10 | 11 | ## Automatic optimistic UI 12 | 13 | Here are some methods from the Todos example app: 14 | 15 | ```js 16 | // Defined in a file that is loaded both on client and server 17 | Meteor.methods({ 18 | '/todos/delete': function (todoId) { 19 | Todos.delete().where({id: todoId}).run(); 20 | }, 21 | '/todos/setChecked': function (todoId, checked) { 22 | Todos.update({checked: checked}).where({id: todoId}).run(); 23 | }, 24 | '/todos/setText': function (todoId, newText) { 25 | Todos.update({text: newText}).where({id: todoId}).run(); 26 | } 27 | }); 28 | ``` 29 | 30 | As you can see, these methods are all very simple operations, and are written entirely using the Knex query builder. 31 | 32 | If you use the subset of Knex operations that are implemented for Meteor's client-side cache, you can get automatic optimistic UI, which is where the same code works on client and server. So in this case, you don't need to do anything special for the UI to update instantly and then get patched with the result from the server. 33 | 34 | For this to work, the columns you are updating inside the method need to be the same in the client-side cache and in the tables in the server-side database. Read more here: [Publishing data to the client](publish.md). 35 | 36 | ## Small differences in the simulation 37 | 38 | In this case, we are operating on data that has an extra column on the client. When we published rows from the `lists` table, we also added an extra field called `incomplete_count`, which was generated by doing a join on the `todos` table. Since the client doesn't know how to do this join, we need to manually update that field on the client. That update is automatically rolled back and replaced with the correct value from the server when the server roundtrip is finished. 39 | 40 | ```js 41 | Meteor.methods({ 42 | '/lists/addTask': function (listId, newTaskText) { 43 | Todos.insert({ 44 | list_id: listId, 45 | text: newTaskText, 46 | checked: false 47 | }).run(); 48 | 49 | if (this.isSimulation) { 50 | // The imcomplete_count column only exists on the client, since it is 51 | // generated from a join/aggregate on the server. We need to update it 52 | // manually 53 | Lists.increment("incomplete_count", 1).where("id", listId).run(); 54 | } 55 | } 56 | }); 57 | ``` 58 | 59 | ## Totally separate simulation or no simulation 60 | 61 | In some cases, the schema might be so different on the server and the client that you need to write a totally separate simulation, or have no simulation at all. 62 | 63 | // XXX write this 64 | 65 | ## Transactions 66 | 67 | One huge benefit of using Postgres over MongoDB is the ability to have transactions. 68 | 69 | // XXX how do you use them 70 | -------------------------------------------------------------------------------- /docs/publish.md: -------------------------------------------------------------------------------- 1 |

Publishing data

2 | 3 | You can publish pretty much any query you can run on your Postgres database, and it will be reactively re-run and updates pushed to the client. Depending on how complex your query is and whether you prefer to use a query builder or a raw SQL string, there are different ways to publish the data. 4 | 5 | Since SQL queries can do joins and aggregates, it's very possible to end up with data on the client which has a different schema than the table on the server. We'll talk about how to deal with that in some of the more complex cases listed below. 6 | 7 | ## Publishing a simple select on a table 8 | 9 | If you just have some rows in your database that you want to use on the client, publishing the data is very simple, and the schema on the client will be the same as on the server. 10 | 11 |

With Knex

12 | 13 | ```js 14 | // Define the table 15 | Todos = new PG.Table("todos"); 16 | 17 | // Publish the data 18 | Meteor.publish('todos', function(listId) { 19 | // Check arguments - note that IDs are integers 20 | check(listId, Match.Integer); 21 | 22 | // Build a query with Knex and return it 23 | return Todos.where("list_id", listId); 24 | }); 25 | ``` 26 | 27 |

With raw query

28 | 29 | Notice that when you don't use the query builder, you need to manually specify 30 | the name of the table with `publishAs` so that Meteor knows what table to put the data in on the 31 | client. 32 | 33 | Read more here: [Knex raw queries](http://knexjs.org/#Raw-Queries). 34 | 35 | ```js 36 | // Define the table 37 | Todos = new PG.Table("todos"); 38 | 39 | // Publish the data 40 | Meteor.publish('todos', function(listId) { 41 | // Check arguments - note that IDs are integers 42 | check(listId, Match.Integer); 43 | 44 | // Build a query with Knex and return it; you need to set the name of the 45 | // table manually 46 | return PG.knex.raw( 47 | "SELECT * FROM todos WHERE list_id=?", 48 | [listId] 49 | ).publishAs("todos"); 50 | }); 51 | ``` 52 | 53 | ## Publishing data with aggregate columns 54 | 55 | If you want to add some extra rows to your database that are aggregates or join results, the schema of your table on the client will be different from that of the server. 56 | 57 | Consider the following query: 58 | 59 | ```sql 60 | select "lists".*, count(todos.id)::integer as incomplete_count 61 | from "lists" 62 | left join "todos" on 63 | "todos"."list_id" = "lists"."id" and 64 | "todos"."checked" = FALSE 65 | where "user_id" is null 66 | group by "lists"."id" 67 | ``` 68 | 69 | This selects rows from the `lists` table, and then adds an extra column called `incomplete_count` which contains the number of `todos` that aren't checked off. 70 | 71 | In this case, on the client the published data will have one more column than the table on the server, so you should consider this when making queries on the client or server. 72 | 73 |

Publishing with Knex

74 | 75 | Here is how you could write this query with Knex (it's a little ugly, I know): 76 | 77 | ```js 78 | Meteor.publish("publicLists", function () { 79 | return Lists 80 | .select("lists.*", PG.knex.raw("count(todos.id)::integer as incomplete_count")) 81 | .where({user_id: null}) 82 | .leftJoin("todos", function () { 83 | this.on("todos.list_id", "lists.id") 84 | .andOn("todos.checked", "=", PG.knex.raw("FALSE")); 85 | }) 86 | .groupBy("lists.id") 87 | .from("lists"); 88 | }) 89 | ``` 90 | 91 |

Publishing with raw SQL query

92 | 93 | Note that you can use ES2015 template strings to write multiline queries nicely. Also, Knex handles writing `is null` for you based on the object you pass to `.where()`, but in raw SQL you have to know ahead of time if the argument is going to be null or not. 94 | 95 | ```js 96 | Meteor.publish("publicLists", function () { 97 | return PG.knex.raw(` 98 | select "lists".*, count(todos.id)::integer as incomplete_count 99 | from "lists" 100 | left join "todos" on 101 | "todos"."list_id" = "lists"."id" and 102 | "todos"."checked" = FALSE 103 | where "user_id" is null 104 | group by "lists"."id" 105 | `).publishAs("lists"); 106 | }) 107 | ``` 108 | 109 | ## Publishing two different views on the same table 110 | 111 | // XXX use publishAs to specify different table names for the client 112 | 113 | -------------------------------------------------------------------------------- /docs/relations.md: -------------------------------------------------------------------------------- 1 |

Working with relational data

2 | 3 | In PostgreSQL, people will often have database schemas that are normalized and rely heavily on foreign keys and joins to assemble data. Here are some strategies to publish data like this to the client in Meteor. 4 | 5 | ## Publishing relational data 6 | 7 | Most of the time, the best thing to do is to publish the individual rows of the appropriate tables to the client, and then do the relational stuff on the client. This will save you from doing super complex joins and aggregates on the server, and will give you maximum flexibility for how to use the data on the client. 8 | 9 | If you want to publish data that includes aggregates or transformations, see the section on that in the [page about publications](publish.md). 10 | 11 | ```js 12 | Meteor.publish("users-posts-and-their-comments", function() { 13 | const userId = this.userId; 14 | 15 | const postsQuery = Posts 16 | .innerJoin("users", "posts.user_id", "users.id") 17 | .where({some: condition}); 18 | 19 | const commentsQuery = Comments 20 | .innerJoin("posts", "comments.post_id", "posts.id") 21 | .innerJoin("users", "posts.user_id", "users.id") 22 | .where({other: condition}); 23 | 24 | return [ 25 | postsQuery, 26 | commentsQuery 27 | ]; 28 | }); 29 | ``` 30 | 31 | Note that we don't have ORM-style relations for the server since they tend to generate inefficient queries, especially in publications. Thankfully doing relations in code on the client is basically free! 32 | 33 | ## Using relational data on the client 34 | 35 | When you declare a table, you can also specify methods that will be attached to every row retrieved from the table on the client. You do this by declaring a class, and then passing it as an option to `PG.Table`, like so: 36 | 37 | ```js 38 | // Declare class 39 | class List extends PG.Model { 40 | todos() { 41 | // Partially built query that encodes the relation 42 | return Todos.where({list_id: this._id}); 43 | } 44 | } 45 | 46 | // Define table, passing in the class as an option 47 | Lists = new PG.Table('lists', { 48 | modelClass: List 49 | }); 50 | ``` 51 | 52 | Now, you can call methods on the rows you retrieve to do simple relations: 53 | 54 | ```js 55 | // Get a list from the table 56 | const list = Lists.where({ id: listId }).fetch()[0]; 57 | 58 | // Get the todo items from the list, sorted by timestamp 59 | const todos = list.todos().orderBy("created_at", "DESC").fetch(), 60 | ``` 61 | 62 | Note that since the `todos()` method returns a partially built query but doesn't call `run()` or `fetch()` on it, you can add additional sorting and filtering before finally executing the query. 63 | -------------------------------------------------------------------------------- /docs/transactions.md: -------------------------------------------------------------------------------- 1 |

Transactions

2 | 3 | One huge benefit of SQL databases over MongoDB is support for transactions, 4 | which let you modify multiple rows in the database in an atomic way. This page 5 | is about how to use them in Meteor; learn more about transactions in the 6 | [PostgreSQL 7 | tutorial](http://www.postgresql.org/docs/9.4/static/tutorial-transactions.html). 8 | 9 | 10 | // XXX write this 11 | // Note that the client-side stub is synchronous, so there is no need for a transaction 12 | // Not clear if methods should be in a transaction by default 13 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1omsoec1aeys7i1hktoi 8 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | insecure 8 | simple:pg 9 | jsx 10 | check 11 | percolate:migrations 12 | env-migrations 13 | aldeed:autoform 14 | 15 | meteor-base 16 | mobile-experience 17 | mongo 18 | blaze-html-templates 19 | session 20 | jquery 21 | tracker 22 | logging 23 | reload 24 | random 25 | ejson 26 | spacebars 27 | 28 | ecmascript 29 | simple:bookshelf 30 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2-rc.3 2 | -------------------------------------------------------------------------------- /examples/old/pg-example/.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:autoform@5.4.0 2 | aldeed:simple-schema@1.1.0 3 | autoupdate@1.2.3-rc.0 4 | babel-compiler@5.8.20-rc.0 5 | babel-runtime@0.1.4-rc.0 6 | base64@1.0.4-rc.0 7 | binary-heap@1.0.4-rc.0 8 | blaze@2.1.3-rc.0 9 | blaze-html-templates@1.0.1-rc.0 10 | blaze-tools@1.0.4-rc.0 11 | boilerplate-generator@1.0.4-rc.0 12 | caching-compiler@1.0.0-rc.0 13 | caching-html-compiler@1.0.1-rc.0 14 | callback-hook@1.0.4-rc.0 15 | check@1.0.6-rc.0 16 | coffeescript@1.0.8-rc.1 17 | cosmos:browserify@0.5.0 18 | dburles:mongo-collection-instances@0.1.3 19 | ddp@1.2.1-rc.0 20 | ddp-client@1.2.1-rc.0 21 | ddp-common@1.2.1-rc.0 22 | ddp-server@1.2.1-rc.0 23 | deps@1.0.8-rc.0 24 | diff-sequence@1.0.1-rc.0 25 | ecmascript@0.1.3-rc.0 26 | ecmascript-collections@0.1.5-rc.0 27 | ejson@1.0.7-rc.0 28 | env-migrations@0.0.1 29 | fastclick@1.0.7-rc.0 30 | geojson-utils@1.0.4-rc.0 31 | hot-code-push@1.0.0-rc.0 32 | html-tools@1.0.5-rc.0 33 | htmljs@1.0.5-rc.0 34 | http@1.1.1-rc.0 35 | id-map@1.0.4-rc.0 36 | insecure@1.0.4-rc.0 37 | jquery@1.11.4-rc.0 38 | jsx@0.1.5 39 | launch-screen@1.0.3-rc.0 40 | livedata@1.0.14-rc.0 41 | logging@1.0.8-rc.0 42 | meteor@1.1.7-rc.0 43 | meteor-base@1.0.1-rc.0 44 | minifiers@1.1.6-rc.1 45 | minimongo@1.0.9-rc.0 46 | mobile-experience@1.0.1-rc.0 47 | mobile-status-bar@1.0.5-rc.0 48 | momentjs:moment@2.10.6 49 | mongo@1.1.1-rc.0 50 | mongo-id@1.0.1-rc.0 51 | npm-mongo@1.4.39-rc.0_1 52 | observe-sequence@1.0.7-rc.0 53 | ordered-dict@1.0.4-rc.0 54 | percolate:migrations@0.7.6 55 | promise@0.4.3-rc.0_1 56 | random@1.0.4-rc.0 57 | reactive-dict@1.1.1-rc.0 58 | reactive-var@1.0.6-rc.0 59 | reload@1.1.4-rc.0 60 | retry@1.0.4-rc.0 61 | routepolicy@1.0.6-rc.0 62 | session@1.1.1-rc.0 63 | simple:bookshelf@0.0.1 64 | simple:pg@0.0.1 65 | spacebars@1.0.7-rc.0 66 | spacebars-compiler@1.0.7-rc.0 67 | templating@1.1.2-rc.1 68 | templating-tools@1.0.0-rc.0 69 | tracker@1.0.8-rc.0 70 | ui@1.0.7-rc.0 71 | underscore@1.0.4-rc.0 72 | url@1.0.5-rc.0 73 | webapp@1.2.2-rc.0 74 | webapp-hashing@1.0.4-rc.0 75 | -------------------------------------------------------------------------------- /examples/old/pg-example/migrations.jsx: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Migrations.add({ 3 | version: 1, 4 | name: 'Create posts and comments table', 5 | up: PG.wrapWithTransaction(function() { 6 | PG.await(PG.knex.schema.createTable("posts", (table) => { 7 | table.increments(); // id 8 | table.string("title"); 9 | })); 10 | 11 | PG.await(PG.knex.schema.createTable("comments", (table) => { 12 | table.increments(); // id 13 | table.string("text"); 14 | table.integer("post_id"); 15 | })); 16 | }), 17 | down: PG.wrapWithTransaction(function() { 18 | PG.await(PG.knex.schema.dropTable("posts")); 19 | PG.await(PG.knex.schema.dropTable("comments")); 20 | }) 21 | }); 22 | 23 | Migrations.add({ 24 | version: 2, 25 | name: 'Create fake posts and comments data', 26 | up: PG.wrapWithTransaction(function() { 27 | _.range(1, 5).forEach((postIndex) => { 28 | const ids = PG.await(PG.knex.table("posts") 29 | .insert({title: `Fake post ${postIndex}`}, "id")); 30 | 31 | _.range(1, 5).forEach((commentIndex) => { 32 | PG.await(PG.knex.table("comments") 33 | .insert({ 34 | text: `Fake comment ${commentIndex} on post ${postIndex}`, 35 | post_id: ids[0] 36 | })); 37 | }); 38 | }); 39 | }), 40 | down: function() { 41 | // LOL not implemented 42 | } 43 | }); 44 | 45 | Migrations.runIfEnvSet(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/old/pg-example/packages: -------------------------------------------------------------------------------- 1 | ../../packages/ -------------------------------------------------------------------------------- /examples/old/pg-example/pg-example.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | -------------------------------------------------------------------------------- /examples/old/pg-example/pg-example.html: -------------------------------------------------------------------------------- 1 | 2 | pg-example 3 | 4 | 5 | 6 | {{> posts}} 7 | {{> allComments}} 8 | 9 | 10 | 23 | 24 | 47 | -------------------------------------------------------------------------------- /examples/old/pg-example/pg-example.jsx: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Things = new Mongo.Collection('things'); 3 | Template.posts.helpers({ 4 | posts: function () { 5 | return Things.find(); 6 | } 7 | }); 8 | 9 | Comments = new Mongo.Collection('comments'); 10 | Template.allComments.helpers({ 11 | comments: function () { 12 | return Comments.find(); 13 | }, 14 | formSchema: function () { 15 | return Schema.newComment; 16 | } 17 | }); 18 | 19 | Meteor.subscribe('mypub'); 20 | Meteor.subscribe('all-comments'); 21 | } 22 | 23 | if (Meteor.isServer) { 24 | Posts = new PG.Table("posts", { 25 | comments() { 26 | return this.hasMany(Comments.model, "post_id"); 27 | } 28 | }); 29 | 30 | Comments = new PG.Table("comments", { 31 | post() { 32 | return this.belongsTo(Posts.model, "post_id"); 33 | } 34 | }); 35 | 36 | Posts.model.where({}).fetch({withRelated: ["comments"]}).then((result) => { 37 | if (! result) { 38 | const post = new Posts.model({ 39 | title: "This is the first post!" 40 | }).save(); 41 | 42 | const comment = new Comments.model({ 43 | text: "This is a comment on the post.", 44 | post_id: 1 45 | }).save(); 46 | } 47 | }); 48 | 49 | Meteor.publish('mypub', function () { 50 | return new PG.Query( 51 | PG.knex 52 | .select("posts.*", 53 | PG.knex.raw("array_to_json(array_agg(comments.*)) as comments")) 54 | .from("posts") 55 | .leftJoin("comments", "comments.post_id", "posts.id") 56 | .groupBy("posts.id") 57 | .toString(), 58 | 'things'); 59 | }); 60 | 61 | Meteor.publish('all-comments', function (startingFrom) { 62 | return Comments.model.where('id', '>', startingFrom || 0); 63 | // also works with pure knex 64 | // return PG.knex.table('comments').where('id', '>', startingFrom || 0); 65 | }); 66 | 67 | 68 | // [ 69 | // 'SELECT posts.*, array_to_json(array_agg(comments.*)) as comments', 70 | // 'FROM posts', 71 | // 'LEFT JOIN comments ON comments.post_id = posts.id', 72 | // 'GROUP BY posts.id' 73 | // ].join(' ') 74 | } 75 | 76 | Schema = {}; 77 | Schema.newComment = new SimpleSchema({ 78 | text: { 79 | type: String, 80 | label: "Comment text", 81 | max: 50 82 | }, 83 | forPost: { 84 | type: Number, 85 | label: "Id of post this comment belongs to" 86 | } 87 | }); 88 | 89 | Meteor.methods({ 90 | addComment(args) { 91 | check(args, Schema.newComment); 92 | 93 | const {text, forPost} = args; 94 | 95 | const doc = { 96 | text, 97 | post_id: forPost 98 | }; 99 | 100 | if (this.isSimulation) { 101 | // we don't have latency compensation yet 102 | return; 103 | Comments.insert(doc); 104 | } else { 105 | PG.await(Comments.model.forge(doc).save()); 106 | } 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/README: -------------------------------------------------------------------------------- 1 | Create database (if you already have Postgres installed and running): 2 | 3 | ```bash 4 | createdb todos 5 | ``` 6 | 7 | Run the knex CLI inside here. 8 | 9 | ```bash 10 | npm install -g knex 11 | npm install 12 | knex migrate:latest 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/knexfile.js: -------------------------------------------------------------------------------- 1 | // Update with your config settings. 2 | 3 | module.exports = { 4 | 5 | development: { 6 | client: 'postgresql', 7 | connection: { 8 | database: 'publish-relation', 9 | user: process.env['USER'] 10 | }, 11 | pool: { 12 | min: 2, 13 | max: 10 14 | }, 15 | migrations: { 16 | tableName: 'knex_migrations' 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/migrations/20150817153755_accounts.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable("users", function (table) { 5 | table.increments(); // integer id 6 | 7 | // XXX POSTGRES 8 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 9 | }), 10 | 11 | knex.schema.createTable("users_services", function (table) { 12 | table.increments(); // integer id 13 | 14 | // XXX POSTGRES 15 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 16 | 17 | table.integer("user_id").notNullable(); 18 | 19 | table.string("service_name").notNullable(); 20 | table.string("key").notNullable(); 21 | table.string("value").notNullable(); 22 | 23 | // We are going to put a random ID here if this value is not meant to be 24 | // unique across users 25 | table.integer("id_if_not_unique").defaultTo(knex.raw("nextval('users_services_id_seq')")); 26 | }), 27 | 28 | knex.schema.createTable("users_emails", function (table) { 29 | table.increments(); // integer id 30 | 31 | // XXX POSTGRES 32 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 33 | 34 | table.integer("user_id").notNullable(); 35 | table.string("address").unique().notNullable(); 36 | table.boolean("verified").defaultTo(false).notNullable(); 37 | }), 38 | 39 | knex.raw("ALTER TABLE users_services ADD CONSTRAINT skvi UNIQUE (service_name, key, value, id_if_not_unique);") 40 | ]); 41 | }; 42 | 43 | exports.down = function(knex, Promise) { 44 | return Promise.all([ 45 | knex.schema.dropTable("users"), 46 | knex.schema.dropTable("users_services"), 47 | knex.schema.dropTable("users_emails") 48 | ]); 49 | }; 50 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/migrations/20150820112203_posts_comments.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable("comments", function (table) { 5 | table.increments(); 6 | table.timestamps(); 7 | 8 | table.integer("user_id"); 9 | table.integer("post_id"); 10 | 11 | table.string("content"); 12 | }), 13 | knex.schema.createTable("posts", function (table) { 14 | table.increments(); 15 | table.timestamps(); 16 | 17 | table.integer("user_id"); 18 | 19 | table.string("content"); 20 | }) 21 | ]); 22 | }; 23 | 24 | exports.down = function(knex, Promise) { 25 | return Promise.all([ 26 | knex.schema.dropTable("comments"), 27 | knex.schema.dropTable("posts") 28 | ]); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/publish-relation/.knex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knex", 3 | "version": "1.0.0", 4 | "description": "Run the knex CLI inside here.", 5 | "main": "knexfile.js", 6 | "dependencies": { 7 | "knex": "^0.8.6", 8 | "pg": "^4.4.1" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | notices-for-0.9.0 10 | notices-for-0.9.1 11 | 0.9.4-platform-file 12 | notices-for-facebook-graph-api-2 13 | 1.2.0-standard-minifiers-package 14 | 1.2.0-meteor-platform-split 15 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1knt9t315s8z4y1ri3uyl 8 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | ecmascript 8 | standard-minifiers 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | tracker 13 | reload 14 | check 15 | 16 | simple:pg 17 | accounts-password 18 | accounts-ui 19 | twbs:bootstrap 20 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | android 4 | ios 5 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2-rc.3 2 | -------------------------------------------------------------------------------- /examples/publish-relation/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.1-rc.0 2 | accounts-base-pg-driver@0.0.1 3 | accounts-password@1.1.2-rc.0 4 | accounts-password-pg-driver@0.0.1 5 | accounts-ui@1.1.6-rc.0 6 | accounts-ui-unstyled@1.1.8-rc.0 7 | autoupdate@1.2.3-rc.0 8 | babel-compiler@5.8.20-rc.0 9 | babel-runtime@0.1.4-rc.0 10 | base64@1.0.4-rc.0 11 | binary-heap@1.0.4-rc.0 12 | blaze@2.1.3-rc.0 13 | blaze-html-templates@1.0.1-rc.0 14 | blaze-tools@1.0.4-rc.0 15 | boilerplate-generator@1.0.4-rc.0 16 | caching-compiler@1.0.0-rc.0 17 | caching-html-compiler@1.0.1-rc.0 18 | callback-hook@1.0.4-rc.0 19 | check@1.0.6-rc.0 20 | coffeescript@1.0.8-rc.1 21 | cosmos:browserify@0.5.0 22 | dburles:mongo-collection-instances@0.1.3 23 | ddp@1.2.1-rc.0 24 | ddp-client@1.2.1-rc.0 25 | ddp-common@1.2.1-rc.0 26 | ddp-rate-limiter@1.0.0-rc.0 27 | ddp-server@1.2.1-rc.0 28 | deps@1.0.8-rc.0 29 | diff-sequence@1.0.1-rc.0 30 | ecmascript@0.1.3-rc.0 31 | ecmascript-collections@0.1.5-rc.0 32 | ejson@1.0.7-rc.0 33 | email@1.0.7-rc.0 34 | fastclick@1.0.7-rc.0 35 | geojson-utils@1.0.4-rc.0 36 | hot-code-push@1.0.0-rc.0 37 | html-tools@1.0.5-rc.0 38 | htmljs@1.0.5-rc.0 39 | http@1.1.1-rc.0 40 | id-map@1.0.4-rc.0 41 | jquery@1.11.4-rc.0 42 | jsx@0.1.5 43 | launch-screen@1.0.3-rc.0 44 | less@2.5.0-rc.1_1 45 | livedata@1.0.14-rc.0 46 | localstorage@1.0.4-rc.0 47 | logging@1.0.8-rc.0 48 | meteor@1.1.7-rc.0 49 | meteor-base@1.0.1-rc.0 50 | minifiers@1.1.6-rc.1 51 | minimongo@1.0.9-rc.0 52 | mobile-experience@1.0.1-rc.0 53 | mobile-status-bar@1.0.5-rc.0 54 | mongo@1.1.1-rc.0 55 | mongo-id@1.0.1-rc.0 56 | npm-bcrypt@0.7.8_2 57 | npm-mongo@1.4.39-rc.0_1 58 | observe-sequence@1.0.7-rc.0 59 | ordered-dict@1.0.4-rc.0 60 | promise@0.4.3-rc.0_1 61 | random@1.0.4-rc.0 62 | rate-limit@1.0.0-rc.0 63 | reactive-dict@1.1.1-rc.0 64 | reactive-var@1.0.6-rc.0 65 | reload@1.1.4-rc.0 66 | retry@1.0.4-rc.0 67 | routepolicy@1.0.6-rc.0 68 | service-configuration@1.0.5-rc.0 69 | session@1.1.1-rc.0 70 | sha@1.0.4-rc.0 71 | simple:bookshelf@0.0.1 72 | simple:pg@0.0.1 73 | spacebars@1.0.7-rc.0 74 | spacebars-compiler@1.0.7-rc.0 75 | srp@1.0.4-rc.0 76 | standard-minifiers@1.0.0-rc.1 77 | templating@1.1.2-rc.1 78 | templating-tools@1.0.0-rc.0 79 | tracker@1.0.8-rc.0 80 | twbs:bootstrap@3.3.5 81 | ui@1.0.7-rc.0 82 | underscore@1.0.4-rc.0 83 | url@1.0.5-rc.0 84 | webapp@1.2.2-rc.0 85 | webapp-hashing@1.0.4-rc.0 86 | -------------------------------------------------------------------------------- /examples/publish-relation/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo of relational publish 3 | 4 | 5 | 6 |
7 |

Relational publish demo

8 | 9 | {{> loginButtons}} 10 | 11 | {{#if currentUser}} 12 |
13 |

Posts

14 |

Comments

15 |
16 | 17 | {{#each posts}} 18 |
19 |
{{content}}
20 |
21 | {{#each comments}} 22 |
{{content}}
23 | {{/each}} 24 |
25 | 26 |
27 |
28 |
29 |
30 | {{/each}} 31 | 32 | {{else}} 33 | Log in to see stuff! 34 | {{/if}} 35 |
36 | 37 | -------------------------------------------------------------------------------- /examples/publish-relation/index.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Template.body.helpers({ 3 | posts() { 4 | return Posts.select(); 5 | } 6 | }); 7 | 8 | Template.body.events({ 9 | "click .add-post"() { 10 | Meteor.call("/posts/insertPost", (err) => { err && alert(err) }); 11 | }, 12 | "click .add-comment"() { 13 | Meteor.call("/comments/insertComment", this.id, (err) => { err && alert(err) }); 14 | } 15 | }); 16 | } 17 | 18 | Meteor.methods({ 19 | "/posts/insertPost"() { 20 | if (! Meteor.userId()) { throw new Meteor.Error("must-log-in") } 21 | if (Meteor.isServer) { 22 | console.log('here'); 23 | Meteor._sleepForMs(1000); 24 | } 25 | 26 | Posts.insert({ 27 | content: Meteor.isServer ? "This is a post!" : "SIMULATION!!!11!!one", 28 | user_id: Meteor.userId() 29 | }).run(); 30 | }, 31 | "/comments/insertComment"(post_id) { 32 | if (! Meteor.userId()) { throw new Meteor.Error("must-log-in") } 33 | 34 | Comments.insert({ 35 | content: "This is a comment!", 36 | user_id: Meteor.userId(), 37 | post_id: post_id 38 | }).run(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /examples/publish-relation/run-app.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | echo "Make sure you have created a DB called 'publish-relation', and that you have run the migrations in .knex/" 3 | export POSTGRESQL_URL="postgres://127.0.0.1/publish-relation" 4 | export PACKAGE_DIRS="$(dirname $0)/../../packages/" 5 | export MONGO_URL="nope" 6 | 7 | cd "$(dirname $0)" 8 | 9 | meteor "$@" 10 | -------------------------------------------------------------------------------- /examples/publish-relation/tables.js: -------------------------------------------------------------------------------- 1 | Comments = new PG.Table("comments"); 2 | 3 | Posts = new PG.Table("posts", { 4 | modelClass: class Post extends PG.Model { 5 | comments() { 6 | return Comments.knex().where({post_id: this.id}); 7 | } 8 | } 9 | }); 10 | 11 | if (Meteor.isServer) { 12 | Meteor.publish("users-posts-and-their-comments", function() { 13 | const userId = parseInt(this.userId, 10); 14 | 15 | if (!userId) { 16 | return null; 17 | } 18 | 19 | // the user -> their posts -> the posts' comments 20 | 21 | const postsQuery = Posts.knex() 22 | .select("posts.*") 23 | .innerJoin("users", "posts.user_id", userId); 24 | 25 | const commentsQuery = Comments.knex() 26 | .select("comments.*") 27 | .innerJoin("posts", "comments.post_id", "posts.id") 28 | .innerJoin("users", "posts.user_id", userId); 29 | 30 | return [ 31 | postsQuery, 32 | commentsQuery 33 | ]; 34 | }); 35 | } else { 36 | Meteor.subscribe("users-posts-and-their-comments"); 37 | } 38 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/README: -------------------------------------------------------------------------------- 1 | Create database (if you already have Postgres installed and running): 2 | 3 | ```bash 4 | createdb todos 5 | ``` 6 | 7 | Run the knex CLI inside here. 8 | 9 | ```bash 10 | npm install -g knex 11 | npm install 12 | knex migrate:latest 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/knexfile.js: -------------------------------------------------------------------------------- 1 | // Update with your config settings. 2 | 3 | module.exports = { 4 | 5 | development: { 6 | client: 'postgresql', 7 | connection: { 8 | database: 'todos', 9 | user: process.env['USER'] 10 | }, 11 | pool: { 12 | min: 2, 13 | max: 10 14 | }, 15 | migrations: { 16 | tableName: 'knex_migrations' 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/migrations/20150817153755_accounts.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable("users", function (table) { 5 | table.increments(); // integer id 6 | 7 | // XXX POSTGRES 8 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 9 | }), 10 | 11 | knex.schema.createTable("users_services", function (table) { 12 | table.increments(); // integer id 13 | 14 | // XXX POSTGRES 15 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 16 | 17 | table.integer("user_id").notNullable(); 18 | 19 | table.string("service_name").notNullable(); 20 | table.string("key").notNullable(); 21 | table.string("value").notNullable(); 22 | 23 | // We are going to put a random ID here if this value is not meant to be 24 | // unique across users 25 | table.integer("id_if_not_unique").defaultTo(knex.raw("nextval('users_services_id_seq')")); 26 | }), 27 | 28 | knex.schema.createTable("users_emails", function (table) { 29 | table.increments(); // integer id 30 | 31 | // XXX POSTGRES 32 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 33 | 34 | table.integer("user_id").notNullable(); 35 | table.string("address").unique().notNullable(); 36 | table.boolean("verified").defaultTo(false).notNullable(); 37 | }), 38 | 39 | knex.raw("ALTER TABLE users_services ADD CONSTRAINT skvi UNIQUE (service_name, key, value, id_if_not_unique);") 40 | ]); 41 | }; 42 | 43 | exports.down = function(knex, Promise) { 44 | return Promise.all([ 45 | knex.schema.dropTable("users"), 46 | knex.schema.dropTable("users_services"), 47 | knex.schema.dropTable("users_emails") 48 | ]); 49 | }; 50 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/migrations/20150817154406_todos.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable("lists", function (table) { 5 | table.increments(); // integer id 6 | 7 | // XXX POSTGRES 8 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 9 | 10 | // It's null if the list is public 11 | table.integer("user_id").nullable(); 12 | 13 | // The name will be the same as the ID 14 | table.string("name").defaultTo(knex.raw("'List '||currval('lists_id_seq')")).notNullable(); 15 | }), 16 | 17 | knex.schema.createTable("todos", function (table) { 18 | table.increments(); // integer id 19 | 20 | // XXX POSTGRES 21 | table.timestamp("created_at").defaultTo(knex.raw('now()')).notNullable(); 22 | 23 | // It's null if the list is public 24 | table.integer("list_id").notNullable(); 25 | table.string("text").notNullable(); 26 | table.boolean("checked").notNullable(); 27 | }) 28 | ]); 29 | }; 30 | 31 | exports.down = function(knex, Promise) { 32 | return Promise.all([ 33 | knex.schema.dropTable("lists"), 34 | knex.schema.dropTable("todos") 35 | ]); 36 | }; 37 | -------------------------------------------------------------------------------- /examples/react-todos/.knex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knex", 3 | "version": "1.0.0", 4 | "description": "Run the knex CLI inside here.", 5 | "main": "knexfile.js", 6 | "dependencies": { 7 | "knex": "^0.8.6", 8 | "pg": "^4.4.1" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | notices-for-0.9.0 10 | notices-for-0.9.1 11 | 0.9.4-platform-file 12 | notices-for-facebook-graph-api-2 13 | 1.2.0-standard-minifiers-package 14 | 1.2.0-meteor-platform-split 15 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1knt9t315s8z4y1ri3uyl 8 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | less 8 | accounts-password-pg-driver 9 | accounts-password 10 | reactive-var 11 | meteorhacks:npm 12 | cosmos:browserify 13 | 14 | 15 | npm-container 16 | ecmascript 17 | standard-minifiers 18 | 19 | meteor-base 20 | mobile-experience 21 | 22 | blaze-html-templates 23 | session 24 | jquery 25 | tracker 26 | logging 27 | reload 28 | random 29 | ejson 30 | spacebars 31 | check 32 | react 33 | 34 | simple:pg 35 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | android 4 | ios 5 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2-rc.3 2 | -------------------------------------------------------------------------------- /examples/react-todos/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.1-rc.0 2 | accounts-base-pg-driver@0.0.1 3 | accounts-password@1.1.2-rc.0 4 | accounts-password-pg-driver@0.0.1 5 | autoupdate@1.2.3-rc.0 6 | babel-compiler@5.8.20-rc.0 7 | babel-runtime@0.1.4-rc.0 8 | base64@1.0.4-rc.0 9 | binary-heap@1.0.4-rc.0 10 | blaze@2.1.3-rc.0 11 | blaze-html-templates@1.0.1-rc.0 12 | blaze-tools@1.0.4-rc.0 13 | boilerplate-generator@1.0.4-rc.0 14 | caching-compiler@1.0.0-rc.0 15 | caching-html-compiler@1.0.1-rc.0 16 | callback-hook@1.0.4-rc.0 17 | check@1.0.6-rc.0 18 | coffeescript@1.0.8-rc.1 19 | cosmos:browserify@0.5.0 20 | dburles:mongo-collection-instances@0.1.3 21 | ddp@1.2.1-rc.0 22 | ddp-client@1.2.1-rc.0 23 | ddp-common@1.2.1-rc.0 24 | ddp-rate-limiter@1.0.0-rc.0 25 | ddp-server@1.2.1-rc.0 26 | deps@1.0.8-rc.0 27 | diff-sequence@1.0.1-rc.0 28 | ecmascript@0.1.3-rc.0 29 | ecmascript-collections@0.1.5-rc.0 30 | ejson@1.0.7-rc.0 31 | email@1.0.7-rc.0 32 | fastclick@1.0.7-rc.0 33 | geojson-utils@1.0.4-rc.0 34 | hot-code-push@1.0.0-rc.0 35 | html-tools@1.0.5-rc.0 36 | htmljs@1.0.5-rc.0 37 | http@1.1.1-rc.0 38 | id-map@1.0.4-rc.0 39 | jquery@1.11.4-rc.0 40 | jsx@0.1.5 41 | launch-screen@1.0.3-rc.0 42 | less@2.5.0-rc.1_1 43 | livedata@1.0.14-rc.0 44 | localstorage@1.0.4-rc.0 45 | logging@1.0.8-rc.0 46 | meteor@1.1.7-rc.0 47 | meteor-base@1.0.1-rc.0 48 | meteorhacks:async@1.0.0 49 | meteorhacks:npm@1.3.0 50 | minifiers@1.1.6-rc.1 51 | minimongo@1.0.9-rc.0 52 | mobile-experience@1.0.1-rc.0 53 | mobile-status-bar@1.0.5-rc.0 54 | mongo@1.1.1-rc.0 55 | mongo-id@1.0.1-rc.0 56 | npm-bcrypt@0.7.8_2 57 | npm-container@1.0.0 58 | npm-mongo@1.4.39-rc.0_1 59 | observe-sequence@1.0.7-rc.0 60 | ordered-dict@1.0.4-rc.0 61 | promise@0.4.3-rc.0_1 62 | random@1.0.4-rc.0 63 | rate-limit@1.0.0-rc.0 64 | react@0.1.9 65 | react-meteor-data@0.1.5 66 | react-runtime@0.13.3_5 67 | react-runtime-dev@0.13.3_4 68 | react-runtime-prod@0.13.3_3 69 | reactive-dict@1.1.1-rc.0 70 | reactive-var@1.0.6-rc.0 71 | reload@1.1.4-rc.0 72 | retry@1.0.4-rc.0 73 | routepolicy@1.0.6-rc.0 74 | session@1.1.1-rc.0 75 | sha@1.0.4-rc.0 76 | simple:bookshelf@0.0.1 77 | simple:pg@0.0.1 78 | spacebars@1.0.7-rc.0 79 | spacebars-compiler@1.0.7-rc.0 80 | srp@1.0.4-rc.0 81 | standard-minifiers@1.0.0-rc.1 82 | templating@1.1.2-rc.1 83 | templating-tools@1.0.0-rc.0 84 | tracker@1.0.8-rc.0 85 | ui@1.0.7-rc.0 86 | underscore@1.0.4-rc.0 87 | url@1.0.5-rc.0 88 | webapp@1.2.2-rc.0 89 | webapp-hashing@1.0.4-rc.0 90 | -------------------------------------------------------------------------------- /examples/react-todos/README.md: -------------------------------------------------------------------------------- 1 | # React Todos 2 | 3 | This example app is a port of the standard Meteor Todos example app to use React for all of the view code. It demostrates a variety of Meteor+React techniques, listed below: 4 | 5 | ### Routing with React Router 6 | 7 | Check out [routes.jsx](client/routes.jsx). You can see how each route corresponds to a React component, and the routes can be nested to make `AppBody` act as a layout. The app is rendered inside a container element on the page (defined in [index.html](index.html)) to avoid conflicting with any libraries that expect to be able to add extra elements to the `body` tag. 8 | 9 | ### Data loading components 10 | 11 | It can be advantageous to limit your data loading logic to a few key components. In this app, those components are [AppBody](client/components/AppBody.jsx) and [TodoListPage](client/components/todo-list/TodoListPage.jsx). They use the `ReactMeteorData` mixin to load data inside a special method called `getMeteorData`, and pass it to their children via `props`. 12 | 13 | ### Manipulating data inside methods 14 | 15 | The Meteor methods we define in [lists.js](lib/lists.js) and [todos.js](lib/todos.js) act as an API or controller layer to our app. We can see all of the data operations done in our app by reading these methods. This would be the best place to add security rules. 16 | 17 | The methods are mostly called from inside event handlers, like in [TodoListHeader](client/components/TodoListHeader.jsx), which is in charge of list title editing, task creation, list privacy settings, and list deletion. 18 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/AppBody.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | Link, 3 | Navigation, 4 | State, 5 | RouteHandler 6 | } = ReactRouter; 7 | 8 | 9 | // true if we should show an error dialog when there is a connection error. 10 | // Exists so that we don't show a connection error dialog when the app is just 11 | // starting and hasn't had a chance to connect yet. 12 | const ShowConnectionIssues = new ReactiveVar(false); 13 | 14 | const CONNECTION_ISSUE_TIMEOUT = 5000; 15 | 16 | 17 | // Only show the connection error box if it has been 5 seconds since 18 | // the app started 19 | setTimeout(function () { 20 | // Show the connection error box 21 | ShowConnectionIssues.set(true); 22 | }, CONNECTION_ISSUE_TIMEOUT); 23 | 24 | 25 | // This component handles making the subscriptons to globally necessary data, 26 | // handling router transitions based on that data, and rendering the basid app 27 | // layout 28 | AppBody = React.createClass({ 29 | mixins: [ReactMeteorData, Navigation, State], 30 | 31 | getInitialState() { 32 | return { 33 | menuOpen: false 34 | }; 35 | }, 36 | 37 | childContextTypes: { 38 | toggleMenuOpen: React.PropTypes.func.isRequired 39 | }, 40 | 41 | getChildContext() { 42 | return { 43 | toggleMenuOpen: this.toggleMenuOpen 44 | } 45 | }, 46 | 47 | getMeteorData() { 48 | const subHandles = [ 49 | Meteor.subscribe("publicLists"), 50 | Meteor.subscribe("privateLists") 51 | ]; 52 | 53 | const subsReady = _.all(subHandles, function (handle) { 54 | return handle.ready(); 55 | }); 56 | 57 | // Get the current routes from React Router 58 | const routes = this.getRoutes(); 59 | // If we are at the root route, and the subscrioptions are ready 60 | if (routes.length > 1 && routes[1].isDefault && subsReady) { 61 | // Redirect to the route for the first todo list 62 | this.replaceWith("todoList", { listId: Lists.fetch()[0].id }); 63 | } 64 | 65 | return { 66 | subsReady: subsReady, 67 | lists: Lists.orderBy("created_at", "DESC").fetch(), 68 | currentUser: Meteor.user(), 69 | disconnected: ShowConnectionIssues.get() && (! Meteor.status().connected) 70 | }; 71 | }, 72 | 73 | toggleMenuOpen() { 74 | this.setState({ 75 | menuOpen: ! this.state.menuOpen 76 | }); 77 | }, 78 | 79 | addList() { 80 | Meteor.call("/lists/add", (err, res) => { 81 | if (err) { 82 | // Not going to be too fancy about error handling in this example app 83 | alert("Error creating list."); 84 | return; 85 | } 86 | 87 | // Go to the page for the new list 88 | this.transitionTo('todoList', { listId: res }); 89 | }); 90 | }, 91 | 92 | getListId() { 93 | return this.getParams().listId; 94 | }, 95 | 96 | render() { 97 | let appBodyContainerClass = ""; 98 | 99 | if (Meteor.isCordova) { 100 | appBodyContainerClass += " cordova"; 101 | } 102 | 103 | if (this.state.menuOpen) { 104 | appBodyContainerClass += " menu-open"; 105 | } 106 | 107 | return ( 108 |
109 | 110 | 115 | 116 | { this.data.disconnected ? : "" } 117 | 118 |
119 | 120 |
121 | { this.data.subsReady ? 122 | : 123 | } 124 |
125 | 126 |
127 | ); 128 | } 129 | }); 130 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/AppLoading.jsx: -------------------------------------------------------------------------------- 1 | AppLoading = React.createClass({ 2 | render() { 3 | return 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/AppNotFound.jsx: -------------------------------------------------------------------------------- 1 | AppNotFound = React.createClass({ 2 | render() { 3 | return ( 4 |
5 | 8 |
9 |
10 |
Page not found
11 |
12 |
13 |
14 | ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/ConnectionIssueDialog.jsx: -------------------------------------------------------------------------------- 1 | ConnectionIssueDialog = React.createClass({ 2 | render() { 3 | // If we needed to display multiple kinds of notifications, we would split 4 | // this out into reusable components, but we only have this one kind so 5 | // we'll keep it all here. 6 | return ( 7 |
8 |
9 | 10 |
11 |
Trying to connect
12 |
13 | There seems to be a connection issue 14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/LeftPanel.jsx: -------------------------------------------------------------------------------- 1 | LeftPanel = React.createClass({ 2 | propTypes: { 3 | currentUser: React.PropTypes.object, 4 | onAddList: React.PropTypes.func, 5 | lists: React.PropTypes.array.isRequired, 6 | activeListId: React.PropTypes.string, 7 | }, 8 | render() { 9 | return ( 10 | 23 | ); 24 | } 25 | }); -------------------------------------------------------------------------------- /examples/react-todos/client/components/MenuOpenToggle.jsx: -------------------------------------------------------------------------------- 1 | MenuOpenToggle = React.createClass({ 2 | contextTypes: { 3 | toggleMenuOpen: React.PropTypes.func.isRequired 4 | }, 5 | render() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | TodoItem = React.createClass({ 2 | propTypes: { 3 | task: React.PropTypes.object.isRequired, 4 | onStopEdit: React.PropTypes.func, 5 | onInitiateEdit: React.PropTypes.func, 6 | beingEdited: React.PropTypes.bool 7 | }, 8 | 9 | getInitialState() { 10 | return { 11 | focused: false, 12 | curText: null 13 | }; 14 | }, 15 | 16 | onFocus() { 17 | this.setState({ 18 | focused: true, 19 | curText: this.props.task.text 20 | }); 21 | this.props.onInitiateEdit(); 22 | }, 23 | 24 | onBlur() { 25 | this.setState({ focused: false }); 26 | this.props.onStopEdit(); 27 | }, 28 | 29 | onTextChange(event) { 30 | const curText = event.target.value; 31 | this.setState({curText: curText}); 32 | 33 | // Throttle updates so we don't go to minimongo and then the server 34 | // on every keystroke. 35 | this.updateText = this.updateText || _.throttle(newText => { 36 | Meteor.call("/todos/setText", this.props.task._id, newText); 37 | }, 300); 38 | 39 | this.updateText(curText); 40 | }, 41 | 42 | onCheckboxChange() { 43 | // Set to the opposite of the current state 44 | const checked = ! this.props.task.checked; 45 | 46 | Meteor.call("/todos/setChecked", this.props.task._id, checked); 47 | }, 48 | 49 | removeThisItem() { 50 | Meteor.call("/todos/delete", this.props.task._id); 51 | }, 52 | 53 | render() { 54 | let className = "list-item"; 55 | 56 | if (this.props.beingEdited) { 57 | className += " editing"; 58 | } 59 | 60 | if (this.props.task.checked) { 61 | className += " checked"; 62 | } 63 | 64 | return ( 65 |
66 | 74 | 81 | 84 | 85 | 86 |
87 | ); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/TodoLists.jsx: -------------------------------------------------------------------------------- 1 | const Link = ReactRouter.Link; 2 | 3 | TodoLists = React.createClass({ 4 | propTypes: { 5 | lists: React.PropTypes.array.isRequired, 6 | activeListId: React.PropTypes.string 7 | }, 8 | 9 | render() { 10 | const allTodoLists = this.props.lists.map((list) => { 11 | let className = "list-todo"; 12 | if (this.props.activeListId === list._id) { 13 | className += " active"; 14 | } 15 | 16 | return ( 17 | 22 | { list.name } 23 | { list.incomplete_count ? 24 | 25 | { list.incomplete_count } 26 | : "" } 27 | 28 | ); 29 | }); 30 | 31 | return ( 32 |
33 | { allTodoLists } 34 |
35 | ); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/accounts/AuthErrors.jsx: -------------------------------------------------------------------------------- 1 | AuthErrors = React.createClass({ 2 | propTypes: { 3 | errors: React.PropTypes.object 4 | }, 5 | render() { 6 | if (this.props.errors) { 7 | return ( 8 |
9 | { 10 | _.values(this.props.errors).map(function (errorMessage) { 11 | return
12 | {errorMessage} 13 |
; 14 | }) 15 | } 16 |
17 | ); 18 | } else { 19 | // Don't render anything 20 | return 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/accounts/AuthFormInput.jsx: -------------------------------------------------------------------------------- 1 | AuthFormInput = React.createClass({ 2 | propTypes: { 3 | hasError: React.PropTypes.bool, 4 | label: React.PropTypes.string, 5 | iconClass: React.PropTypes.string, 6 | type: React.PropTypes.string, 7 | name: React.PropTypes.string 8 | }, 9 | render() { 10 | let className = "input-symbol"; 11 | if (this.props.hasError) { 12 | className += " error"; 13 | } 14 | 15 | return ( 16 |
17 | 21 | 22 | 25 |
26 | ); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/accounts/AuthJoinPage.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | Navigation, 3 | Link 4 | } = ReactRouter; 5 | 6 | AuthJoinPage = React.createClass({ 7 | mixins: [Navigation], 8 | getInitialState() { 9 | return { 10 | errors: {} 11 | }; 12 | }, 13 | onSubmit(event) { 14 | event.preventDefault(); 15 | 16 | const email = event.target.email.value; 17 | const password = event.target.password.value; 18 | const confirm = event.target.confirm.value; 19 | 20 | const errors = {}; 21 | 22 | if (! email) { 23 | errors.email = 'Email required'; 24 | } 25 | 26 | if (! password) { 27 | errors.password = 'Password required'; 28 | } 29 | 30 | if (confirm !== password) { 31 | errors.confirm = 'Please confirm your password'; 32 | } 33 | 34 | this.setState({ 35 | errors: errors 36 | }); 37 | 38 | if (! _.isEmpty(errors)) { 39 | // Form errors found, do not create user 40 | return; 41 | } 42 | 43 | Accounts.createUser({ 44 | email: email, 45 | password: password 46 | }, (error) => { 47 | if (error) { 48 | this.setState({ 49 | errors: { 'none': error.reason } 50 | }); 51 | 52 | return; 53 | } 54 | 55 | this.transitionTo('root'); 56 | }); 57 | }, 58 | render() { 59 | return ( 60 |
61 | 64 | 65 |
66 |
67 |

Join.

68 |

69 | Joining allows you to make private lists 70 |

71 | 72 |
73 | 74 | 75 | 81 | 82 | 88 | 89 | 95 | 96 | 99 | 100 |
101 | 102 | 103 | Have an account? Sign in. 104 | 105 |
106 |
107 | ); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/accounts/AuthSignInPage.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | Navigation, 3 | Link 4 | } = ReactRouter; 5 | 6 | AuthSignInPage = React.createClass({ 7 | mixins: [Navigation], 8 | getInitialState() { 9 | return { 10 | errors: {} 11 | }; 12 | }, 13 | onSubmit(event) { 14 | event.preventDefault(); 15 | 16 | const email = event.target.email.value; 17 | const password = event.target.password.value; 18 | 19 | const errors = {}; 20 | 21 | if (! email) { 22 | errors.email = 'Email required'; 23 | } 24 | 25 | if (! password) { 26 | errors.password = 'Password required'; 27 | } 28 | 29 | this.setState({ 30 | errors: errors 31 | }); 32 | 33 | if (! _.isEmpty(errors)) { 34 | // Form errors found, do not log in 35 | return; 36 | } 37 | 38 | Meteor.loginWithPassword(email, password, (error) => { 39 | if (error) { 40 | this.setState({ 41 | errors: { 'none': error.reason } 42 | }); 43 | 44 | return; 45 | } 46 | 47 | this.transitionTo('root'); 48 | }); 49 | }, 50 | render() { 51 | return
52 | 55 | 56 |
57 |
58 |

Sign In.

59 |

60 | Signing in allows you to view private lists 61 |

62 | 63 |
64 | 65 | 66 | 72 | 73 | 78 | 79 | 82 | 83 |
84 | 85 | Need an account? Join Now. 86 | 87 |
88 |
89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/accounts/UserSidebarSection.jsx: -------------------------------------------------------------------------------- 1 | const Link = ReactRouter.Link; 2 | 3 | UserSidebarSection = React.createClass({ 4 | getInitialState() { 5 | return { 6 | menuOpen: false 7 | }; 8 | }, 9 | 10 | propTypes: { 11 | user: React.PropTypes.object 12 | }, 13 | 14 | toggleMenuOpen(event) { 15 | event.preventDefault(); 16 | 17 | this.setState({ 18 | menuOpen: ! this.state.menuOpen 19 | }); 20 | }, 21 | 22 | logout() { 23 | Meteor.logout(); 24 | }, 25 | 26 | render() { 27 | let contents; 28 | 29 | if (this.props.user) { 30 | const email = this.props.user.emails[0].address; 31 | const emailUsername = email.substring(0, email.indexOf('@')); 32 | 33 | const arrowDirection = this.state.menuOpen ? "up" : "down"; 34 | const arrowIconClass = "icon-arrow-" + arrowDirection; 35 | 36 | contents = ( 37 |
38 | 39 | 40 | { emailUsername } 41 | 42 | { this.state.menuOpen ? 43 | Logout : ""} 44 |
45 | ); 46 | } else { 47 | contents = ( 48 |
49 | Sign in 50 | Join 51 |
52 | ); 53 | } 54 | 55 | return ( 56 |
57 | { contents } 58 |
59 | ); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/todo-list/TodoListItems.jsx: -------------------------------------------------------------------------------- 1 | TodoListItems = React.createClass({ 2 | propTypes: { 3 | tasks: React.PropTypes.array.isRequired 4 | }, 5 | 6 | getInitialState() { 7 | return { 8 | taskBeingEditedId: null, 9 | }; 10 | }, 11 | 12 | setTaskBeingEdited(taskId) { 13 | this.setState({ 14 | taskBeingEditedId: taskId 15 | }); 16 | }, 17 | 18 | render() { 19 | var allTodoItems = this.props.tasks.map((task) => { 20 | return ( 21 | 27 | ); 28 | }); 29 | 30 | return ( 31 |
32 | { allTodoItems } 33 |
34 | ); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /examples/react-todos/client/components/todo-list/TodoListPage.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | Navigation, 3 | State 4 | } = ReactRouter; 5 | 6 | TodoListPage = React.createClass({ 7 | mixins: [ReactMeteorData, Navigation, State], 8 | 9 | getMeteorData() { 10 | // Get list ID from ReactRouter 11 | const listId = parseInt(this.getParams().listId, 10); 12 | 13 | // Subscribe to the tasks we need to render this component 14 | const tasksSubHandle = Meteor.subscribe("todos", listId); 15 | 16 | const list = Lists.where({ id: listId }).fetch()[0]; 17 | 18 | if (tasksSubHandle.ready()) { 19 | return { 20 | tasks: list && list.todos().orderBy("created_at", "DESC").fetch(), 21 | list: list, 22 | tasksLoading: false 23 | }; 24 | } else { 25 | return { 26 | list: list, 27 | tasksLoading: true, 28 | }; 29 | } 30 | }, 31 | 32 | render() { 33 | const list = this.data.list; 34 | const tasks = this.data.tasks; 35 | 36 | if (! list) { 37 | return ; 38 | } 39 | 40 | return ( 41 |
42 | 45 | 46 | { this.data.tasksLoading ? "" : 47 | 48 | } 49 |
50 | ); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /examples/react-todos/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Todos - All your todos synced wherever you happen to be 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/react-todos/client/lib/app.browserify.js: -------------------------------------------------------------------------------- 1 | ReactRouter = require("react-router"); -------------------------------------------------------------------------------- /examples/react-todos/client/lib/app.browserify.options.json: -------------------------------------------------------------------------------- 1 | { 2 | "transforms": { 3 | "externalify": { 4 | "global": true, 5 | "external": { 6 | "react": "React.require" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /examples/react-todos/client/lib/jquery.touchwipe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!) 3 | * Common usage: wipe images (left and right to show the previous or next image) 4 | * 5 | * @author Andreas Waltl, netCU Internetagentur (http://www.netcu.de) 6 | * @version 1.1.1 (9th December 2010) - fix bug (older IE's had problems) 7 | * @version 1.1 (1st September 2010) - support wipe up and wipe down 8 | * @version 1.0 (15th July 2010) 9 | */ 10 | (function($) { 11 | $.fn.touchwipe = function(settings) { 12 | var config = { 13 | min_move_x: 20, 14 | min_move_y: 20, 15 | wipeLeft: function() { }, 16 | wipeRight: function() { }, 17 | wipeUp: function() { }, 18 | wipeDown: function() { }, 19 | preventDefaultEvents: true 20 | }; 21 | 22 | if (settings) $.extend(config, settings); 23 | 24 | this.each(function() { 25 | var startX; 26 | var startY; 27 | var isMoving = false; 28 | 29 | function cancelTouch() { 30 | this.removeEventListener('touchmove', onTouchMove); 31 | startX = null; 32 | isMoving = false; 33 | } 34 | 35 | function onTouchMove(e) { 36 | if(config.preventDefaultEvents) { 37 | e.preventDefault(); 38 | } 39 | if(isMoving) { 40 | var x = e.touches[0].pageX; 41 | var y = e.touches[0].pageY; 42 | var dx = startX - x; 43 | var dy = startY - y; 44 | if(Math.abs(dx) >= config.min_move_x) { 45 | cancelTouch(); 46 | if(dx > 0) { 47 | config.wipeLeft(); 48 | } 49 | else { 50 | config.wipeRight(); 51 | } 52 | } 53 | else if(Math.abs(dy) >= config.min_move_y) { 54 | cancelTouch(); 55 | if(dy > 0) { 56 | config.wipeDown(); 57 | } 58 | else { 59 | config.wipeUp(); 60 | } 61 | } 62 | } 63 | } 64 | 65 | function onTouchStart(e) 66 | { 67 | if (e.touches.length == 1) { 68 | startX = e.touches[0].pageX; 69 | startY = e.touches[0].pageY; 70 | isMoving = true; 71 | this.addEventListener('touchmove', onTouchMove, false); 72 | } 73 | } 74 | if ('ontouchstart' in document.documentElement) { 75 | this.addEventListener('touchstart', onTouchStart, false); 76 | } 77 | }); 78 | 79 | return this; 80 | }; 81 | 82 | })(jQuery); 83 | -------------------------------------------------------------------------------- /examples/react-todos/client/routes.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | Route, 3 | NotFoundRoute, 4 | DefaultRoute 5 | } = ReactRouter; 6 | 7 | const routes = ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | const router = ReactRouter.create({ 18 | routes: routes, 19 | location: ReactRouter.HistoryLocation 20 | }); 21 | 22 | Meteor.startup(function () { 23 | router.run(function (Handler, state) { 24 | React.render(, document.getElementById("app-container")); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/components/app-not-found.import.less: -------------------------------------------------------------------------------- 1 | .page.not-found { 2 | .content-scrollable { background: @color-tertiary; } 3 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/components/auth.import.less: -------------------------------------------------------------------------------- 1 | .page.auth { 2 | text-align: center; 3 | 4 | .content-scrollable { background: @color-tertiary; } 5 | 6 | .wrapper-auth { 7 | padding-top: 4em; 8 | 9 | @media screen and (min-width: 40em) { 10 | margin: 0 auto; 11 | max-width: 480px; 12 | width: 80%; 13 | } 14 | 15 | .title-auth { 16 | .font-l1; 17 | .type-light; 18 | color: @color-ancillary; 19 | margin-bottom: .75rem; 20 | } 21 | 22 | .subtitle-auth { 23 | color: @color-medium-well; 24 | margin: 0 15% 3rem; 25 | } 26 | 27 | form { 28 | .input-symbol { 29 | margin-bottom: 1px; 30 | width: 100%; 31 | } 32 | 33 | .btn-primary { 34 | margin: 1em 5% 0; 35 | width: 90%; 36 | 37 | @media screen and (min-width: 40em) { 38 | margin-left: 0; 39 | margin-right: 0; 40 | width: 100%; 41 | } 42 | } 43 | } 44 | .list-errors { 45 | margin-top: -2rem; 46 | .list-item { 47 | .title-caps; 48 | background: @color-note; 49 | color: @color-negative; 50 | font-size: .625em; // 10px 51 | margin-bottom: 1px; 52 | padding: .7rem 0; 53 | } 54 | } 55 | } 56 | 57 | .link-auth-alt { 58 | .font-s1; 59 | .position(absolute, auto, 0, 1em, 0); 60 | color: @color-medium; 61 | display: inline-block; 62 | 63 | @media screen and (min-width: 40em) { 64 | bottom: 0; 65 | margin-top: 1rem; 66 | position: relative; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/components/lists-show.import.less: -------------------------------------------------------------------------------- 1 | .page.lists-show { 2 | .content-scrollable { 3 | background: @color-empty; 4 | top: 5em !important; 5 | } 6 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/components/loading.import.less: -------------------------------------------------------------------------------- 1 | .loading-app { 2 | .position(absolute, 50%, 50%, auto, auto, 50%); 3 | .transform(translate3d(50%, -50%, 0)); 4 | min-width: 160px; 5 | max-width: 320px; 6 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/base.import.less: -------------------------------------------------------------------------------- 1 | * { 2 | .box-sizing(border-box); 3 | -webkit-tap-highlight-color:rgba(0,0,0,0); 4 | -webkit-tap-highlight-color: transparent; // for some Androids 5 | } 6 | 7 | html, button, input, textarea, select { 8 | outline: none; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | body { 14 | .type-regular; 15 | color: @color-full; 16 | font-size: 16px; //this sets the baseline so we can use multiples of 4 & (r)ems 17 | } 18 | 19 | // Default type layout 20 | h1, h2, h3, h4, h5, h6 { 21 | .type-regular; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | h1 { 27 | .font-l1; 28 | } 29 | 30 | h2 { 31 | .font-m3; 32 | } 33 | 34 | h3 { 35 | .font-m2; 36 | } 37 | 38 | h4 { 39 | .font-m1; 40 | } 41 | 42 | h5 { 43 | .font-s2; 44 | color: @color-medium-rare; 45 | text-transform: uppercase; 46 | } 47 | 48 | h6 { 49 | color: @color-medium; 50 | } 51 | 52 | p { 53 | .font-s3; 54 | } 55 | 56 | sub, 57 | sup { 58 | font-size: .8em; 59 | } 60 | 61 | sub { 62 | bottom: -.2em; 63 | } 64 | 65 | sup { 66 | top: -.2em; 67 | } 68 | 69 | b { 70 | font-weight: bold; 71 | } 72 | 73 | em { 74 | font-style: italic; 75 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/button.import.less: -------------------------------------------------------------------------------- 1 | [class^="btn-"], 2 | [class*=" btn-"] { 3 | // Sizing 4 | .font-s2; 5 | line-height: 20px !important; //override line-height always so we can use em's to size 6 | padding: 1em 1.25em; // 48px tall 7 | 8 | // Style 9 | .title-caps; 10 | .transition( all 200ms ease-in ); 11 | color: @color-empty; 12 | display: inline-block; 13 | position: relative; 14 | text-align: center; 15 | text-decoration: none !important; //prevents global styles from applying 16 | vertical-align: middle; 17 | white-space: nowrap; 18 | 19 | &[class*="primary"] { 20 | background-color: @color-primary; 21 | color: @color-empty; 22 | 23 | &:hover { background-color: darken(@color-primary, 5%); } 24 | &:active { box-shadow: rgba(0,0,0,.3) 0 1px 3px 0 inset; } 25 | } 26 | 27 | &[class*="secondary"] { 28 | .transition( all 300ms ease-in ); 29 | box-shadow: lighten(#517096, 5%) 0 0 0 1px inset; 30 | color: @color-empty; 31 | 32 | &:hover{ color: @color-rare; } 33 | &:active, 34 | &.active { 35 | box-shadow: lighten(#517096, 25%) 0 0 0 1px inset; 36 | } 37 | } 38 | 39 | &[disabled] { opacity: .5; } 40 | } 41 | 42 | .btns-group { 43 | .display(flex); 44 | .flex-wrap(wrap); 45 | width: 100%; 46 | 47 | [class*="btn-"] { 48 | .ellipsized; 49 | .flex(1); 50 | 51 | & + [class*="btn-"] { margin-left: -1px; } 52 | } 53 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/form.import.less: -------------------------------------------------------------------------------- 1 | // Standard text input 2 | input[type="text"], 3 | input[type="email"], 4 | input[type="password"], 5 | textarea { 6 | // Sizing 7 | .font-s2; 8 | .type-regular; 9 | padding: .75rem 0; //total height ~48 10 | line-height: 1.5rem !important; 11 | 12 | // Style 13 | .placeholder(@color-complementary); 14 | border: none; 15 | border-radius: 0; 16 | box-sizing: border-box; 17 | color: @color-full; 18 | outline: none; 19 | 20 | &[disabled] { opacity: .5; } 21 | } 22 | 23 | // Remove chrome/saf autofill yellow background 24 | input:-webkit-autofill { 25 | -webkit-box-shadow: 0 0 0 1000px @color-empty inset; 26 | } 27 | 28 | // Custom checkbox 29 | .checkbox { 30 | display: inline-block; 31 | height: 3rem; 32 | position: relative; 33 | vertical-align: middle; 34 | width: 44px; 35 | 36 | input[type="checkbox"] { 37 | font-size: 1em; 38 | visibility: hidden; 39 | 40 | & + span:before { 41 | .position(absolute, 50%, auto, auto, 50%, .85em, .85em); 42 | .transform(translate3d(-50%, -50%, 0)); 43 | background: transparent; 44 | box-shadow: #abdfe3 0 0 0 1px inset; 45 | content: ''; 46 | display: block; 47 | } 48 | 49 | &:checked + span:before { 50 | box-shadow: none; 51 | color: @color-medium-rare; 52 | 53 | // Icon family from icon.lessimport 54 | font-family: 'todos'; 55 | speak: none; 56 | font-style: normal; 57 | font-weight: normal; 58 | font-variant: normal; 59 | text-transform: none; 60 | line-height: 1; 61 | 62 | // Better Font Rendering 63 | -webkit-font-smoothing: antialiased; 64 | -moz-osx-font-smoothing: grayscale; 65 | 66 | // Checkmark icon 67 | content: "\e612"; 68 | } 69 | } 70 | } 71 | 72 | // Input with an icon 73 | .input-symbol { 74 | display: inline-block; 75 | position: relative; 76 | 77 | &.error [class^="icon-"], 78 | &.error [class*=" icon-"] { 79 | color: @color-negative; 80 | } 81 | 82 | // Position & padding 83 | [class^="icon-"], 84 | [class*=" icon-"] { 85 | left: 1em; 86 | } 87 | 88 | input { padding-left: 3em; } 89 | 90 | // Styling 91 | input { 92 | width: 100%; 93 | 94 | &:focus { 95 | & + [class^="icon-"], 96 | & + [class*=" icon-"] { 97 | color: @color-primary; 98 | } 99 | } 100 | } 101 | 102 | [class^="icon-"], 103 | [class*=" icon-"] { 104 | .transition( all 300ms ease-in ); 105 | .transform(translate3d(0,-50%,0)); 106 | background: transparent; 107 | color: @color-medium; 108 | font-size: 1em; 109 | height: 1em; 110 | position: absolute; 111 | top: 50%; 112 | width: 1em; 113 | } 114 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/icon.import.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'todos'; 3 | src:url('icon/todos.eot?-5w3um4'); 4 | src:url('icon/todos.eot?#iefix5w3um4') format('embedded-opentype'), 5 | url('icon/todos.woff?5w3um4') format('woff'), 6 | url('icon/todos.ttf?5w3um4') format('truetype'), 7 | url('icon/todos.svg?5w3um4#todos') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: 'todos'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | // Better Font Rendering 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | 27 | .icon-unlock:before { 28 | content: "\e600"; 29 | } 30 | .icon-user-add:before { 31 | content: "\e604"; 32 | } 33 | .icon-cog:before { 34 | content: "\e606"; 35 | } 36 | .icon-trash:before { 37 | content: "\e607"; 38 | } 39 | .icon-edit:before { 40 | content: "\e608"; 41 | } 42 | .icon-add:before { 43 | content: "\e60a"; 44 | } 45 | .icon-plus:before { 46 | content: "\e60b"; 47 | } 48 | .icon-close:before { 49 | content: "\e60c"; 50 | } 51 | .icon-cross:before { 52 | content: "\e60d"; 53 | } 54 | .icon-sync:before { 55 | content: "\e60e"; 56 | } 57 | .icon-lock:before { 58 | content: "\e610"; 59 | } 60 | .icon-check:before { 61 | content: "\e612"; 62 | } 63 | .icon-share:before { 64 | content: "\e617"; 65 | } 66 | .icon-email:before { 67 | content: "\e619"; 68 | } 69 | .icon-arrow-up:before { 70 | content: "\e623"; 71 | } 72 | .icon-arrow-down:before { 73 | content: "\e626"; 74 | } 75 | .icon-list-unordered:before { 76 | content: "\e634"; 77 | } 78 | -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/layout.import.less: -------------------------------------------------------------------------------- 1 | @menu-width: 270px; 2 | @column: 5.55555%; 3 | 4 | body { 5 | .position(absolute, 0, 0, 0, 0); 6 | background-color: #315481; 7 | .background-image( linear-gradient(top, #315481, #918e82 100%) ); 8 | background-repeat: no-repeat; 9 | background-attachment: fixed; 10 | } 11 | 12 | #container { 13 | .position(absolute, 0, 0, 0, 0); 14 | 15 | @media screen and (min-width: 60em) { 16 | left: @column; 17 | right: @column; 18 | } 19 | 20 | @media screen and (min-width: 80em) { 21 | left: 2*@column; 22 | right: 2*@column; 23 | } 24 | 25 | // Hide anything offscreen 26 | overflow: hidden; 27 | } 28 | 29 | #menu { 30 | .position(absolute, 0, 0, 0, 0, @menu-width); 31 | } 32 | 33 | #content-container { 34 | .position(absolute, 0, 0, 0, 0); 35 | .transition(all 200ms ease-out); 36 | .transform(translate3d(0, 0, 0)); 37 | background: @color-tertiary; 38 | opacity: 1; 39 | 40 | @media screen and (min-width: 40em) { 41 | left: @menu-width; 42 | } 43 | 44 | .content-scrollable { 45 | .position(absolute, 0, 0, 0, 0); 46 | .transform(translate3d(0, 0, 0)); 47 | overflow-y: auto; 48 | -webkit-overflow-scrolling: touch; 49 | } 50 | 51 | // Toggle menu on mobile 52 | .menu-open & { 53 | .transform(translate3d(@menu-width, 0, 0)); 54 | opacity: .85; 55 | left: 0; 56 | 57 | @media screen and (min-width: 40em) { 58 | // Show menu on desktop, negate .menu-open 59 | .transform(translate3d(0, 0, 0)); //reset transform and use position properties instead 60 | opacity: 1; 61 | left: @menu-width; 62 | } 63 | } 64 | } 65 | 66 | // Transparent screen to prevent interactions on content when menu is open 67 | .content-overlay { 68 | .position(absolute, 0, 0, 0, 0); 69 | cursor: pointer; 70 | 71 | .menu-open & { 72 | .transform(translate3d(@menu-width, 0, 0)); 73 | z-index: 1; 74 | } 75 | 76 | // Hide overlay on desktop 77 | @media screen and (min-width: 40em) { display: none; } 78 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/link.import.less: -------------------------------------------------------------------------------- 1 | a { 2 | .transition( all 200ms ease-in ); 3 | color: @color-secondary; 4 | cursor: pointer; 5 | text-decoration: none; 6 | 7 | &:hover { color: darken(@color-primary, 10%); } 8 | &:active { color: @color-well; } 9 | &:focus { outline:none; } //removes FF dotted outline 10 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/list-items.import.less: -------------------------------------------------------------------------------- 1 | .list-items .list-item { 2 | .font-s2; 3 | 4 | // Layout of list-item children 5 | .display(flex); 6 | .flex-wrap(wrap); 7 | height: 3rem; 8 | width: 100%; 9 | 10 | .checkbox { 11 | .flex(0, 0, 44px); 12 | cursor: pointer; 13 | } 14 | input[type="text"] { .flex(1); } 15 | .delete-item { .flex(0, 0, 3rem); } 16 | 17 | 18 | // Style of list-item children 19 | input[type="text"] { 20 | background: transparent; 21 | cursor: pointer; 22 | 23 | &:focus { cursor: text; } 24 | } 25 | 26 | .delete-item { 27 | color: @color-medium-rare; 28 | line-height: 3rem; 29 | text-align: center; 30 | 31 | &:hover { color: @color-primary; } 32 | &:active { color: @color-well; } 33 | .icon-trash { font-size: 1.1em; } 34 | } 35 | 36 | 37 | // Border between list items 38 | & + .list-item { border-top: 1px solid #f0f9fb; } 39 | 40 | // Checked 41 | &.checked { 42 | input[type="text"] { 43 | color: @color-medium-rare; 44 | text-decoration: line-through; 45 | } 46 | 47 | .delete-item { display: inline-block; } 48 | } 49 | 50 | // Editing 51 | .delete-item { display: none; } 52 | &.editing .delete-item { display: inline-block; } 53 | } 54 | -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/menu.import.less: -------------------------------------------------------------------------------- 1 | #menu { 2 | overflow-y: auto; 3 | -webkit-overflow-scrolling: touch; 4 | 5 | .btns-group, 6 | .btns-group-vertical { 7 | margin: 2em auto 2em; 8 | width: 80%; 9 | 10 | .btn-secondary { 11 | .font-s1; 12 | padding-top: .5em; 13 | padding-bottom: .5em; 14 | } 15 | } 16 | 17 | .btns-group-vertical .btn-secondary { 18 | .force-wrap; 19 | padding-right: 2.5em; 20 | text-align: left; 21 | text-indent: 0; 22 | white-space: normal; // Resets wrapping 23 | width: 100%; 24 | 25 | & + .btn-secondary { 26 | margin-top: .5rem; 27 | 28 | &:before { 29 | .position(absolute, -.5rem, 50%, auto, auto, 1px, .5rem); 30 | background: lighten(#517096, 5%); 31 | content: ''; 32 | } 33 | } 34 | 35 | [class^="icon-"], 36 | [class*=" icon-"] { 37 | .position(absolute, .5em, .5em, auto, auto); 38 | line-height: 20px; 39 | } 40 | } 41 | 42 | .list-todos { 43 | a { 44 | box-shadow: rgba(255,255,255,.15) 0 1px 0 0; 45 | display: block; 46 | line-height: 1.5em; 47 | padding: .75em 2.5em; 48 | position: relative; 49 | } 50 | 51 | .count-list { 52 | .transition( all 200ms ease-in ); 53 | background: rgba(255,255,255,.1); 54 | border-radius: 1em; 55 | float: right; 56 | font-size: .7rem; 57 | line-height: 1; 58 | margin-top: .25rem; 59 | margin-right: -1.5em; 60 | padding: .3em .5em; 61 | } 62 | 63 | [class^="icon-"], 64 | [class*=" icon-"] { 65 | .font-s2; 66 | float: left; 67 | margin-left: -1.5rem; 68 | margin-right: .5rem; 69 | margin-top: .1rem; 70 | width: 1em; 71 | } 72 | 73 | .icon-lock { 74 | .font-s1; 75 | margin-top: .2rem; 76 | opacity: .8; 77 | } 78 | 79 | .list-todo { 80 | color: rgba(255,255,255,.4); 81 | 82 | &:hover, 83 | &:active, 84 | &.active { 85 | color: @color-empty; 86 | .count-list { background: @color-primary; } 87 | } 88 | 89 | .cordova &:hover { 90 | // Prevent hover states from being noticeable on Cordova apps 91 | color: rgba(255,255,255,.4); 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/message.import.less: -------------------------------------------------------------------------------- 1 | // Empty states and 404 messages 2 | .wrapper-message { 3 | .position(absolute, 45%, 0, auto, 0); 4 | .transform(translate3d(0, -50%, 0)); 5 | text-align: center; 6 | 7 | .title-message { 8 | .font-m2; 9 | .type-light; 10 | color: @color-ancillary; 11 | margin-bottom: .5em; 12 | } 13 | 14 | .subtitle-message { 15 | .font-s2; 16 | color: @color-medium; 17 | } 18 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/nav.import.less: -------------------------------------------------------------------------------- 1 | // Generic nav positioning and styles 2 | nav { 3 | .position(absolute, 0, 0, auto, 0); 4 | .transform(translate3d(0,0,0)); 5 | .transition( all 200ms ease-out ); 6 | z-index: 10; 7 | 8 | .nav-item { 9 | .font-m1; 10 | color: @color-ancillary; 11 | display: inline-block; 12 | height: 3rem; 13 | text-align: center; 14 | width: 3rem; 15 | 16 | &:active { opacity: .5; } 17 | 18 | [class^="icon-"], 19 | [class*=" icon-"] { 20 | line-height: 3rem; 21 | vertical-align: middle; 22 | } 23 | } 24 | .nav-group { 25 | .position(absolute, 0, auto, auto, 0); 26 | z-index: 1; 27 | 28 | &.right { 29 | left: auto; 30 | right: 0; 31 | } 32 | } 33 | } 34 | 35 | // Custom nav for lists-show 36 | .page.lists-show nav { 37 | .background-image( linear-gradient(top, #d0edf5, #e1e5f0 100%) ); 38 | height: 5em; 39 | 40 | text-align: center; 41 | @media screen and (min-width: 40em) { text-align: left; } 42 | 43 | .title-page { 44 | .position(absolute, 0, 3rem, auto, 3rem); 45 | @media screen and (min-width: 40em) { 46 | left: 1rem; 47 | right: 6rem; 48 | } 49 | 50 | cursor: pointer; 51 | font-size: 1.125em; // 18px 52 | white-space: nowrap; 53 | 54 | .title-wrapper { 55 | .ellipsized; 56 | color: @color-ancillary; 57 | display: inline-block; 58 | padding-right: 1.5rem; 59 | vertical-align: top; 60 | max-width: 100%; 61 | } 62 | 63 | .count-list { 64 | background: @color-primary; 65 | border-radius: 1em; 66 | color: @color-empty; 67 | display: inline-block; 68 | font-size: .7rem; 69 | line-height: 1; 70 | margin-left: -1.25rem; 71 | margin-top: -4px; 72 | padding: .3em .5em; 73 | vertical-align: middle; 74 | } 75 | } 76 | form.todo-new { 77 | .position(absolute, 3em, 0, auto, 0); 78 | 79 | input[type="text"] { 80 | background: transparent; 81 | padding-bottom: .25em; 82 | padding-left: 44px !important; 83 | padding-top: .25em; 84 | } 85 | } 86 | form.list-edit-form { 87 | position: relative; 88 | 89 | input[type="text"] { 90 | background: transparent; 91 | font-size: 1.125em; // 18px 92 | width: 100%; 93 | padding-right: 3em; 94 | padding-left: 1rem; 95 | } 96 | } 97 | 98 | select.list-edit { 99 | .font-s2; 100 | .position(absolute, 0,0,0,0); 101 | background: transparent; 102 | opacity: 0; // allows the cog to appear 103 | } 104 | 105 | .options-web { 106 | display: none; 107 | 108 | .nav-item { 109 | .font-s3; 110 | width: 2rem; 111 | 112 | &:last-child { margin-right: .5rem; } 113 | } 114 | } 115 | 116 | // Hide & show options and nav icons 117 | @media screen and (min-width: 40em) { 118 | .nav-group:not(.right) { display: none !important; } 119 | .options-mobile { display: none; } 120 | .options-web { display: block; } 121 | } 122 | } 123 | 124 | 125 | // Custom nav for auth 126 | @media screen and (min-width: 40em) { 127 | .page.auth .nav-group { display: none; } 128 | .page.not-found .nav-group { display: none; } 129 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/globals/notification.import.less: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes spin { 2 | 0% { .transform(rotate(0deg)); } 3 | 100% { .transform(rotate(359deg)); } 4 | } 5 | @keyframes spin { 6 | 0% { .transform(rotate(0deg)); } 7 | 100% { .transform(rotate(359deg)); } 8 | } 9 | 10 | 11 | // Notification message (e.g., when unable to connect) 12 | .notifications { 13 | .position(absolute, auto, auto, 10px, 50%, 280px); 14 | .transform(translate3d(-50%, 0, 0)); 15 | z-index: 1; 16 | 17 | @media screen and (min-width: 40em) { 18 | .transform(translate3d(0, 0, 0)); 19 | bottom: auto; 20 | right: 1rem; 21 | top: 1rem; 22 | left: auto; 23 | } 24 | 25 | .notification { 26 | .font-s1; 27 | background: rgba(51,51,51, .85); 28 | color: @color-empty; 29 | margin-bottom: .25rem; 30 | padding: .5rem .75rem; 31 | position: relative; 32 | width: 100%; 33 | 34 | .icon-sync { 35 | .position(absolute, 30%, auto, auto, 1rem); 36 | .animation(spin 2s infinite linear); 37 | color: @color-empty; 38 | font-size: 1.5em; 39 | } 40 | 41 | .meta { 42 | overflow: hidden; 43 | padding-left: 3em; 44 | 45 | .title-notification { 46 | .title-caps; 47 | display: block; 48 | } 49 | 50 | .description { 51 | display: block; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/main.less: -------------------------------------------------------------------------------- 1 | @import 'util/reset.import.less'; 2 | 3 | // Mixins & utilities 4 | @import 'util/helpers.import.less'; 5 | @import 'util/lesshat.import.less'; 6 | @import 'util/fontface.import.less'; 7 | @import 'util/text.import.less'; 8 | @import 'util/typography.import.less'; 9 | @import 'util/variables.import.less'; 10 | 11 | // Global namespace 12 | @import 'globals/base.import.less'; 13 | @import 'globals/button.import.less'; 14 | @import 'globals/form.import.less'; 15 | @import 'globals/icon.import.less'; 16 | @import 'globals/layout.import.less'; 17 | @import 'globals/link.import.less'; 18 | @import 'globals/menu.import.less'; 19 | @import 'globals/nav.import.less'; 20 | 21 | // Global templates 22 | @import 'globals/list-items.import.less'; 23 | @import 'globals/message.import.less'; 24 | @import 'globals/notification.import.less'; 25 | 26 | // Templates 27 | @import 'components/lists-show.import.less'; 28 | @import 'components/auth.import.less'; 29 | @import 'components/app-not-found.import.less'; 30 | @import 'components/loading.import.less'; 31 | -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/fontface.import.less: -------------------------------------------------------------------------------- 1 | // Light 2 | @font-face { 3 | font-family: 'Open Sans'; 4 | src: url('font/OpenSans-Light-webfont.eot'); 5 | src: url('font/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 6 | url('font/OpenSans-Light-webfont.woff') format('woff'), 7 | url('font/OpenSans-Light-webfont.ttf') format('truetype'), 8 | url('font/OpenSans-Light-webfont.svg#OpenSansLight') format('svg'); 9 | font-weight: 200; 10 | font-style: normal; 11 | } 12 | 13 | // Regular 14 | @font-face { 15 | font-family: 'Open Sans'; 16 | src: url('font/OpenSans-Regular-webfont.eot'); 17 | src: url('font/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 18 | url('font/OpenSans-Regular-webfont.woff') format('woff'), 19 | url('font/OpenSans-Regular-webfont.ttf') format('truetype'), 20 | url('font/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg'); 21 | font-weight: normal; 22 | font-weight: 400; 23 | font-style: normal; 24 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/helpers.import.less: -------------------------------------------------------------------------------- 1 | .position(@type; @top: auto; @right: auto; @bottom: auto; @left: auto; @width: auto; @height: auto) { 2 | position: @type; 3 | top: @top; 4 | right: @right; 5 | bottom: @bottom; 6 | left: @left; 7 | width: @width; 8 | height: @height; 9 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/reset.import.less: -------------------------------------------------------------------------------- 1 | /* Reset.less 2 | * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). 3 | * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ 4 | 5 | 6 | // ERIC MEYER RESET 7 | // -------------------------------------------------- 8 | 9 | html, body { margin: 0; padding: 0; } 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, cite, code, del, dfn, em, img, q, s, samp, small, strike, strong, sub, sup, tt, var, dd, dl, dt, li, ol, ul, fieldset, form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; font-weight: normal; font-style: normal; font-size: 100%; line-height: 1; font-family: inherit; } 11 | table { border-collapse: collapse; border-spacing: 0; } 12 | ol, ul { list-style: none; } 13 | q:before, q:after, blockquote:before, blockquote:after { content: ""; } 14 | 15 | 16 | // Normalize.css 17 | // Pulling in select resets form the normalize.css project 18 | // -------------------------------------------------- 19 | 20 | // Display in IE6-9 and FF3 21 | // ------------------------- 22 | // Source: http://github.com/necolas/normalize.css 23 | html { 24 | font-size: 100%; 25 | -webkit-text-size-adjust: 100%; 26 | -ms-text-size-adjust: 100%; 27 | } 28 | // Focus states 29 | a:focus { 30 | outline: thin dotted; 31 | } 32 | // Hover & Active 33 | a:hover, 34 | a:active { 35 | outline: 0; 36 | } 37 | 38 | // Display in IE6-9 and FF3 39 | // ------------------------- 40 | // Source: http://github.com/necolas/normalize.css 41 | article, 42 | aside, 43 | details, 44 | figcaption, 45 | figure, 46 | footer, 47 | header, 48 | hgroup, 49 | nav, 50 | section { 51 | display: block; 52 | } 53 | 54 | // Display block in IE6-9 and FF3 55 | // ------------------------- 56 | // Source: http://github.com/necolas/normalize.css 57 | audio, 58 | canvas, 59 | video { 60 | display: inline-block; 61 | *display: inline; 62 | *zoom: 1; 63 | } 64 | 65 | // Prevents modern browsers from displaying 'audio' without controls 66 | // ------------------------- 67 | // Source: http://github.com/necolas/normalize.css 68 | audio:not([controls]) { 69 | display: none; 70 | } 71 | 72 | // Prevents sub and sup affecting line-height in all browsers 73 | // ------------------------- 74 | // Source: http://github.com/necolas/normalize.css 75 | sub, 76 | sup { 77 | font-size: 75%; 78 | line-height: 0; 79 | position: relative; 80 | vertical-align: baseline; 81 | } 82 | sup { 83 | top: -0.5em; 84 | } 85 | sub { 86 | bottom: -0.25em; 87 | } 88 | 89 | // Img border in a's and image quality 90 | // ------------------------- 91 | // Source: http://github.com/necolas/normalize.css 92 | img { 93 | border: 0; 94 | -ms-interpolation-mode: bicubic; 95 | } 96 | 97 | // Forms 98 | // ------------------------- 99 | // Source: http://github.com/necolas/normalize.css 100 | 101 | // Font size in all browsers, margin changes, misc consistency 102 | button, 103 | input, 104 | select, 105 | textarea { 106 | font-size: 100%; 107 | margin: 0; 108 | vertical-align: baseline; 109 | *vertical-align: middle; 110 | } 111 | button, 112 | input { 113 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet 114 | *overflow: visible; // Inner spacing ie IE6/7 115 | } 116 | button::-moz-focus-inner, 117 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4 118 | border: 0; 119 | padding: 0; 120 | } 121 | button, 122 | input[type="button"], 123 | input[type="reset"], 124 | input[type="submit"] { 125 | cursor: pointer; // Cursors on all buttons applied consistently 126 | -webkit-appearance: button; // Style clicable inputs in iOS 127 | } 128 | input[type="search"] { // Appearance in Safari/Chrome 129 | -webkit-appearance: textfield; 130 | -webkit-box-sizing: content-box; 131 | -moz-box-sizing: content-box; 132 | box-sizing: content-box; 133 | } 134 | input[type="search"]::-webkit-search-decoration { 135 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5 136 | } 137 | textarea { 138 | overflow: auto; // Remove vertical scrollbar in IE6-9 139 | vertical-align: top; // Readability and alignment cross-browser 140 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/text.import.less: -------------------------------------------------------------------------------- 1 | // Caps styling used in headers 2 | .title-caps() { 3 | letter-spacing: .3em; 4 | text-indent: .3em; 5 | text-transform: uppercase; 6 | } 7 | 8 | // Adds an ellipses at the end of overflowing strings 9 | .ellipsized() { 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | } 14 | 15 | .force-wrap { 16 | word-wrap: break-word; 17 | word-break: break-all; 18 | -ms-word-break: break-all; 19 | word-break: break-word; // Non-standard for webkit 20 | .hyphens(auto); 21 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/typography.import.less: -------------------------------------------------------------------------------- 1 | .type-regular() { 2 | font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-style: 400; 4 | } 5 | 6 | .type-light { 7 | font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-weight: 300; 9 | } 10 | 11 | 12 | // Large fonts 13 | .font-l3() { 14 | font-size: 56px; 15 | line-height: 64px; 16 | } 17 | 18 | .font-l2() { 19 | font-size: 48px; 20 | line-height: 56px; 21 | } 22 | 23 | .font-l1() { 24 | font-size: 40px; 25 | line-height: 48px; 26 | } 27 | 28 | // Medium fonts 29 | .font-m3() { 30 | font-size: 28px; 31 | line-height: 32px; 32 | } 33 | 34 | .font-m2() { 35 | font-size: 24px; 36 | line-height: 28px; 37 | } 38 | 39 | .font-m1() { 40 | font-size: 20px; 41 | line-height: 24px; 42 | } 43 | 44 | // Small fonts 45 | .font-s3() { 46 | font-size: 16px; 47 | line-height: 24px; 48 | } 49 | 50 | .font-s2() { 51 | font-size: 14px; 52 | line-height: 20px; 53 | } 54 | 55 | .font-s1() { 56 | font-size: 12px; 57 | line-height: 16px; 58 | } -------------------------------------------------------------------------------- /examples/react-todos/client/stylesheets/util/variables.import.less: -------------------------------------------------------------------------------- 1 | // Core 2 | @color-primary: #2cc5d2; //caribbean teal (buttons) 3 | @color-secondary: #5db9ff; //cerulean blue (menu new list) 4 | @color-tertiary: #d2edf4; //muted teal (join/signin bg) 5 | @color-ancillary: #1c3f53; //deep navy (nav heading, menu icon) 6 | @color-complementary: #778b91; //muted navy (add item placeholder) 7 | 8 | // Alert 9 | @color-negative: #ff4400; //error, alert 10 | @color-note: #f6fccf; 11 | 12 | // Greyscale 13 | @color-empty: white; 14 | @color-raw: #f8f8f8; 15 | @color-raw: #f2f2f2; 16 | @color-rare: #eee; 17 | @color-medium-rare: #ccc; 18 | @color-medium: #aaa; 19 | @color-medium-well: #666; 20 | @color-well: #555; 21 | @color-full: #333; -------------------------------------------------------------------------------- /examples/react-todos/lib/lists.js: -------------------------------------------------------------------------------- 1 | class List extends PG.Model { 2 | todos() { 3 | return Todos.where({list_id: this.id}); 4 | } 5 | } 6 | 7 | Lists = new PG.Table('lists', { 8 | modelClass: List 9 | }); 10 | 11 | Meteor.methods({ 12 | '/lists/add': function () { 13 | return Lists.insert({}).returning("id").run()[0]; 14 | }, 15 | '/lists/updateName': function (listId, newName) { 16 | Lists.update({name: newName}).where({id: listId}).run(); 17 | }, 18 | '/lists/addTask': function (listId, newTaskText) { 19 | const todo = { 20 | list_id: listId, 21 | text: newTaskText, 22 | checked: false 23 | }; 24 | 25 | if (this.isSimulation) { 26 | // Since the database generates the timestamp, we need to do it manually 27 | // in the simulation 28 | todo.created_at = new Date(); 29 | 30 | // Since this field is generated via a join, we have to update it manually 31 | // on the client 32 | Lists.where("id", listId).increment("incomplete_count", 1).run(); 33 | } 34 | 35 | Todos.insert(todo).run(); 36 | } 37 | }); 38 | 39 | if (Meteor.isClient) { 40 | // Stuff below here is server-only 41 | return; 42 | } 43 | 44 | Meteor.methods({ 45 | '/lists/togglePrivate': function (listId) { 46 | var list = Lists.where({id: listId}).run()[0]; 47 | 48 | if (! Meteor.userId()) { 49 | throw new Meteor.Error("not-logged-in"); 50 | } 51 | 52 | if (list.user_id) { 53 | Lists.where({id: listId}).update({user_id: null}).run(); 54 | } else { 55 | // ensure the last public list cannot be made private 56 | if (! list.user_id && Lists.count("*").whereNull("user_id").run()[0].count == 1) { 57 | throw new Meteor.Error("final-list-private"); 58 | } 59 | 60 | Lists.where({id: listId}).update({user_id: Meteor.userId()}).run(); 61 | } 62 | }, 63 | '/lists/delete': function (listId) { 64 | var list = Lists.where({id: listId}).run()[0]; 65 | 66 | // ensure the last public list cannot be deleted. 67 | if (! list.user_id && Lists.count("*").whereNull("user_id").run()[0].count == 1) { 68 | throw new Meteor.Error("final-list-delete"); 69 | } 70 | 71 | PG.inTransaction(() => { 72 | // Make sure to delete all of the items 73 | Todos.where({list_id: listId}).delete().run(); 74 | 75 | // Delete the list itself 76 | Lists.where({id: listId}).delete().run(); 77 | }); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /examples/react-todos/lib/todos.js: -------------------------------------------------------------------------------- 1 | Todos = new PG.Table('todos'); 2 | 3 | Meteor.methods({ 4 | '/todos/delete': function (todoId) { 5 | Todos.delete().where({id: todoId}).run(); 6 | }, 7 | '/todos/setChecked': function (todoId, checked) { 8 | Todos.update({checked: checked}).where({id: todoId}).run(); 9 | }, 10 | '/todos/setText': function (todoId, newText) { 11 | Todos.update({text: newText}).where({id: todoId}).run(); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /examples/react-todos/mobile-config.js: -------------------------------------------------------------------------------- 1 | App.info({ 2 | name: 'Todos', 3 | description: 'A simple todo list app built in Meteor.', 4 | author: 'Percolate Studio Team', 5 | email: 'us@percolatestudio.com', 6 | website: 'http://percolatestudio.com', 7 | version: '0.0.1' 8 | }); 9 | 10 | App.icons({ 11 | // iOS 12 | 'iphone': 'resources/icons/icon-60x60.png', 13 | 'iphone_2x': 'resources/icons/icon-60x60@2x.png', 14 | 'ipad': 'resources/icons/icon-72x72.png', 15 | 'ipad_2x': 'resources/icons/icon-72x72@2x.png', 16 | 17 | // Android 18 | 'android_ldpi': 'resources/icons/icon-36x36.png', 19 | 'android_mdpi': 'resources/icons/icon-48x48.png', 20 | 'android_hdpi': 'resources/icons/icon-72x72.png', 21 | 'android_xhdpi': 'resources/icons/icon-96x96.png' 22 | }); 23 | 24 | App.launchScreens({ 25 | // iOS 26 | 'iphone': 'resources/splash/splash-320x480.png', 27 | 'iphone_2x': 'resources/splash/splash-320x480@2x.png', 28 | 'iphone5': 'resources/splash/splash-320x568@2x.png', 29 | 'ipad_portrait': 'resources/splash/splash-768x1024.png', 30 | 'ipad_portrait_2x': 'resources/splash/splash-768x1024@2x.png', 31 | 'ipad_landscape': 'resources/splash/splash-1024x768.png', 32 | 'ipad_landscape_2x': 'resources/splash/splash-1024x768@2x.png', 33 | 34 | // Android 35 | 'android_ldpi_portrait': 'resources/splash/splash-200x320.png', 36 | 'android_ldpi_landscape': 'resources/splash/splash-320x200.png', 37 | 'android_mdpi_portrait': 'resources/splash/splash-320x480.png', 38 | 'android_mdpi_landscape': 'resources/splash/splash-480x320.png', 39 | 'android_hdpi_portrait': 'resources/splash/splash-480x800.png', 40 | 'android_hdpi_landscape': 'resources/splash/splash-800x480.png', 41 | 'android_xhdpi_portrait': 'resources/splash/splash-720x1280.png', 42 | 'android_xhdpi_landscape': 'resources/splash/splash-1280x720.png' 43 | }); 44 | 45 | App.setPreference('StatusBarOverlaysWebView', 'false'); 46 | App.setPreference('StatusBarBackgroundColor', '#000000'); 47 | 48 | -------------------------------------------------------------------------------- /examples/react-todos/packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "react-router": "0.13.3", 3 | "externalify": "0.1.0" 4 | } -------------------------------------------------------------------------------- /examples/react-todos/packages/npm-container/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/react-todos/packages/npm-container/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /examples/react-todos/packages/npm-container/index.js: -------------------------------------------------------------------------------- 1 | Meteor.npmRequire = function(moduleName) { 2 | var module = Npm.require(moduleName); 3 | return module; 4 | }; 5 | 6 | Meteor.require = function(moduleName) { 7 | console.warn('Meteor.require is deprecated. Please use Meteor.npmRequire instead!'); 8 | return Meteor.npmRequire(moduleName); 9 | }; -------------------------------------------------------------------------------- /examples/react-todos/packages/npm-container/package.js: -------------------------------------------------------------------------------- 1 | var path = Npm.require('path'); 2 | var fs = Npm.require('fs'); 3 | 4 | Package.describe({ 5 | summary: 'Contains all your npm dependencies', 6 | version: '1.0.0', 7 | name: 'npm-container' 8 | }); 9 | 10 | var packagesJsonFile = path.resolve('./packages.json'); 11 | try { 12 | var fileContent = fs.readFileSync(packagesJsonFile); 13 | var packages = JSON.parse(fileContent.toString()); 14 | Npm.depends(packages); 15 | } catch (ex) { 16 | console.error('ERROR: packages.json parsing error [ ' + ex.message + ' ]'); 17 | } 18 | 19 | // Adding the app's packages.json as a used file for this package will get 20 | // Meteor to watch it and reload this package when it changes 21 | Package.onUse(function(api) { 22 | api.add_files('index.js', 'server'); 23 | api.add_files('../../packages.json', 'server', {isAsset: true}); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/react-todos/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /examples/react-todos/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/favicon.png -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Light-webfont.ttf -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Regular-webfont.ttf -------------------------------------------------------------------------------- /examples/react-todos/public/font/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/font/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /examples/react-todos/public/icon/todos.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/icon/todos.eot -------------------------------------------------------------------------------- /examples/react-todos/public/icon/todos.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/icon/todos.ttf -------------------------------------------------------------------------------- /examples/react-todos/public/icon/todos.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/public/icon/todos.woff -------------------------------------------------------------------------------- /examples/react-todos/public/img/logo-todos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 42 | 48 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-29x29.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-29x29@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-36x36.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-40x40.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-40x40@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-48x48.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-50x50.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-50x50@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-57x57.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-57x57@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-60x60.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-60x60@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-72x72.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-72x72@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-76x76.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-76x76@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/icons/icon-96x96.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-1024x768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-1024x768.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-1024x768@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-1024x768@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-1280x720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-1280x720.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-200x320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-200x320.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-320x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-320x200.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-320x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-320x480.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-320x480@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-320x480@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-320x568@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-320x568@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-480x320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-480x320.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-480x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-480x800.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-720x1280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-720x1280.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-768x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-768x1024.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-768x1024@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-768x1024@2x.png -------------------------------------------------------------------------------- /examples/react-todos/resources/splash/splash-800x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/examples/react-todos/resources/splash/splash-800x480.png -------------------------------------------------------------------------------- /examples/react-todos/run-app.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | echo "Make sure you have created a DB called 'todos', and that you have run the migrations in .knex/" 3 | export POSTGRESQL_URL="postgres://127.0.0.1/todos" 4 | export PACKAGE_DIRS="$(dirname $0)/../../packages/" 5 | export MONGO_URL="nope" 6 | 7 | cd "$(dirname $0)" 8 | 9 | meteor "$@" 10 | -------------------------------------------------------------------------------- /examples/react-todos/server/bootstrap.js: -------------------------------------------------------------------------------- 1 | // if the database is empty on server start, create some sample data. 2 | Meteor.startup(function () { 3 | if (PG.await(PG.knex("lists").count("*"))[0].count == 0) { 4 | var data = [ 5 | {name: "Meteor Principles", 6 | items: ["Data on the Wire", 7 | "One Language", 8 | "Database Everywhere", 9 | "Latency Compensation", 10 | "Full Stack Reactivity", 11 | "Embrace the Ecosystem", 12 | "Simplicity Equals Productivity" 13 | ] 14 | }, 15 | {name: "Languages", 16 | items: ["Lisp", 17 | "C", 18 | "C++", 19 | "Python", 20 | "Ruby", 21 | "JavaScript", 22 | "Scala", 23 | "Erlang", 24 | "6502 Assembly" 25 | ] 26 | }, 27 | {name: "Favorite Scientists", 28 | items: ["Ada Lovelace", 29 | "Grace Hopper", 30 | "Marie Curie", 31 | "Carl Friedrich Gauss", 32 | "Nikola Tesla", 33 | "Claude Shannon" 34 | ] 35 | } 36 | ]; 37 | 38 | var timestamp = (new Date()).getTime(); 39 | 40 | _.each(data, function(list) { 41 | 42 | var list_id = PG.await(PG.knex("lists").insert({ 43 | name: list.name 44 | }).returning("id"))[0]; 45 | 46 | _.each(list.items, function(text) { 47 | PG.await(PG.knex("todos").insert({ 48 | list_id: list_id, 49 | text: text, 50 | created_at: new Date(timestamp), 51 | checked: false 52 | })); 53 | 54 | timestamp += 1; // ensure unique timestamp. 55 | }); 56 | }); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /examples/react-todos/server/publish.js: -------------------------------------------------------------------------------- 1 | Meteor.publish('publicLists', function() { 2 | return getListWhere({user_id: null}); 3 | }); 4 | 5 | Meteor.publish('privateLists', function() { 6 | if (this.userId) { 7 | return getListWhere({user_id: parseInt(this.userId, 10)}); 8 | } else { 9 | this.ready(); 10 | } 11 | }); 12 | 13 | Meteor.publish('todos', function(listId) { 14 | check(listId, Match.Integer); 15 | 16 | return Todos 17 | .select("*") 18 | .from("todos") 19 | .where("list_id", listId); 20 | }); 21 | 22 | function getListWhere(where) { 23 | return Lists 24 | .select("lists.*", PG.knex.raw("count(todos.id)::integer as incomplete_count")) 25 | .where(where) 26 | .leftJoin("todos", function () { 27 | this.on("todos.list_id", "lists.id") 28 | .andOn("todos.checked", "=", PG.knex.raw("FALSE")); 29 | }) 30 | .groupBy("lists.id") 31 | .from("lists"); 32 | } 33 | -------------------------------------------------------------------------------- /img/dumbo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/img/dumbo.jpg -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Meteor PostgreSQL 2 | theme: readthedocs 3 | pages: 4 | - Introduction: index.md 5 | - DB setup and migrations: migrations.md 6 | - Running SQL queries with Knex: knex.md 7 | - Client-side data cache: client.md 8 | - Publishing data to the client: publish.md 9 | - Writing to the database with methods: methods.md 10 | - Relational data: relations.md 11 | - Using transactions inside methods: transactions.md 12 | - Contribute: contribute.md 13 | -------------------------------------------------------------------------------- /packages/accounts-base-pg-driver/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/packages/accounts-base-pg-driver/README.md -------------------------------------------------------------------------------- /packages/accounts-base-pg-driver/accounts-base-pg-driver-tests.js: -------------------------------------------------------------------------------- 1 | // let accountsDBClient; 2 | // Tinytest.add('run migrations up', function (test) { 3 | // PG.wrapWithTransaction(AccountsDBClientPG.migrations.up)(); 4 | // accountsDBClient = new AccountsDBClientPG(); 5 | // }); 6 | 7 | // let userId; 8 | // Tinytest.add('insert user', function (test) { 9 | // userId = accountsDBClient.insertUser(); 10 | // let user = accountsDBClient.getUserById(userId); 11 | // console.log(user); 12 | // }); 13 | 14 | // Tinytest.add('run migrations down', function (test) { 15 | // PG.wrapWithTransaction(AccountsDBClientPG.migrations.down)(); 16 | // }); 17 | -------------------------------------------------------------------------------- /packages/accounts-base-pg-driver/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'accounts-base-pg-driver', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.1.0.3'); 15 | api.addFiles('accounts-base-pg-driver.js'); 16 | api.use([ 17 | 'simple:pg', 18 | 'ecmascript', 19 | 'check', 20 | 'underscore' 21 | ], 'server'); 22 | 23 | api.export("AccountsDBClientPG"); 24 | }); 25 | 26 | Package.onTest(function(api) { 27 | api.use([ 28 | 'simple:pg', 29 | 'tinytest', 30 | 'accounts-base-pg-driver', 31 | 'ecmascript' 32 | ]); 33 | api.addFiles('accounts-base-pg-driver-tests.js', 'server'); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/accounts-base/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/accounts-base/README.md: -------------------------------------------------------------------------------- 1 | # accounts-base 2 | 3 | Meteor's user account system. This package implements the basic functions necessary for user accounts and lets other packages register login services. Some of these services are in the following packages: 4 | 5 | - `accounts-password` 6 | - `accounts-facebook` 7 | - `accounts-google` 8 | - `accounts-github` 9 | - `accounts-twitter` 10 | - `accounts-meetup` 11 | - `accounts-weibo` 12 | 13 | There are also login services available in community packages. 14 | 15 | For more information, see the [Meteor docs](http://docs.meteor.com/#accounts_api) and the Meteor Accounts [project page](https://www.meteor.com/accounts). 16 | -------------------------------------------------------------------------------- /packages/accounts-base/accounts_rate_limit.js: -------------------------------------------------------------------------------- 1 | var Ap = AccountsCommon.prototype; 2 | var defaultRateLimiterRuleId; 3 | // Removes default rate limiting rule 4 | Ap.removeDefaultRateLimit = function () { 5 | const resp = DDPRateLimiter.removeRule(defaultRateLimiterRuleId); 6 | defaultRateLimiterRuleId = null; 7 | return resp; 8 | } 9 | 10 | // Add a default rule of limiting logins, creating new users and password reset 11 | // to 5 times every 10 seconds per connection. 12 | Ap.addDefaultRateLimit = function () { 13 | if (!defaultRateLimiterRuleId) { 14 | defaultRateLimiterRuleId = DDPRateLimiter.addRule({ 15 | userId: null, 16 | clientAddress: null, 17 | type: 'method', 18 | name: function (name) { 19 | return _.contains(['login', 'createUser', 'resetPassword', 20 | 'forgotPassword'], name); 21 | }, 22 | connectionId: function (connectionId) { 23 | return true; 24 | } 25 | }, 5, 10000); 26 | } 27 | } 28 | 29 | Ap.addDefaultRateLimit(); -------------------------------------------------------------------------------- /packages/accounts-base/accounts_url_tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add("accounts - parse urls for accounts-password", 2 | function (test) { 3 | var actions = ["reset-password", "verify-email", "enroll-account"]; 4 | 5 | // make sure the callback was called the right number of times 6 | var actionsParsed = []; 7 | 8 | _.each(actions, function (hashPart) { 9 | var fakeToken = "asdf"; 10 | 11 | var hashTokenOnly = "#/" + hashPart + "/" + fakeToken; 12 | AccountsTest.attemptToMatchHash(hashTokenOnly, function (token, action) { 13 | test.equal(token, fakeToken); 14 | test.equal(action, hashPart); 15 | 16 | // XXX COMPAT WITH 0.9.3 17 | if (hashPart === "reset-password") { 18 | test.equal(Accounts._resetPasswordToken, fakeToken); 19 | } else if (hashPart === "verify-email") { 20 | test.equal(Accounts._verifyEmailToken, fakeToken); 21 | } else if (hashPart === "enroll-account") { 22 | test.equal(Accounts._enrollAccountToken, fakeToken); 23 | } 24 | 25 | // Reset variables for the next test 26 | Accounts._resetPasswordToken = null; 27 | Accounts._verifyEmailToken = null; 28 | Accounts._enrollAccountToken = null; 29 | 30 | actionsParsed.push(action); 31 | }); 32 | }); 33 | 34 | // make sure each action is called once, in order 35 | test.equal(actionsParsed, actions); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/accounts-base/globals_client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace Accounts 3 | * @summary The namespace for all client-side accounts-related methods. 4 | */ 5 | Accounts = new AccountsClient(); 6 | 7 | /** 8 | * @summary A [Mongo.Collection](#collections) containing user documents. 9 | * @locus Anywhere 10 | * @type {Mongo.Collection} 11 | */ 12 | Meteor.users = Accounts.users; 13 | -------------------------------------------------------------------------------- /packages/accounts-base/globals_server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace Accounts 3 | * @summary The namespace for all server-side accounts-related methods. 4 | */ 5 | Accounts = new AccountsServer(Meteor.server); 6 | 7 | // Users table. Don't use the normal autopublish, since we want to hide 8 | // some fields. Code to autopublish this is in accounts_server.js. 9 | // XXX Allow users to configure this collection name. 10 | 11 | /** 12 | * @summary A [Mongo.Collection](#collections) containing user documents. 13 | * @locus Anywhere 14 | * @type {Mongo.Collection} 15 | */ 16 | Meteor.users = Accounts.users; 17 | -------------------------------------------------------------------------------- /packages/accounts-base/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "A user account system", 3 | version: "1.2.1-rc.0" 4 | }); 5 | 6 | Package.onUse(function (api) { 7 | api.use('underscore', ['client', 'server']); 8 | api.use('ddp-rate-limiter'); 9 | api.use('localstorage', 'client'); 10 | api.use('tracker', 'client'); 11 | api.use('check', 'server'); 12 | api.use('random', ['client', 'server']); 13 | api.use('ejson', 'server'); 14 | api.use('callback-hook', ['client', 'server']); 15 | 16 | // use unordered to work around a circular dependency 17 | // (service-configuration needs Accounts.connection) 18 | // api.use('service-configuration', ['client', 'server'], { unordered: true }); 19 | 20 | // needed for getting the currently logged-in user 21 | api.use('ddp', ['client', 'server']); 22 | 23 | // need this because of the Meteor.users collection but in the future 24 | // we'd probably want to abstract this away 25 | api.use('mongo', ['client']); 26 | 27 | // If the 'blaze' package is loaded, we'll define some helpers like 28 | // {{currentUser}}. If not, no biggie. 29 | api.use('blaze', 'client', {weak: true}); 30 | 31 | // Allow us to detect 'autopublish', and publish some Meteor.users fields if 32 | // it's loaded. 33 | api.use('autopublish', 'server', {weak: true}); 34 | 35 | api.use('oauth-encryption', 'server', {weak: true}); 36 | 37 | api.export('Accounts'); 38 | api.export('AccountsClient', 'client'); 39 | api.export('AccountsServer', 'server'); 40 | api.export('AccountsTest', {testOnly: true}); 41 | 42 | api.addFiles('accounts_common.js', ['client', 'server']); 43 | api.addFiles('accounts_server.js', 'server'); 44 | 45 | api.addFiles('accounts_rate_limit.js'); 46 | api.addFiles('url_server.js', 'server'); 47 | 48 | // accounts_client must be before localstorage_token, because 49 | // localstorage_token attempts to call functions in accounts_client (eg 50 | // Accounts.callLoginMethod) on startup. And localstorage_token must be after 51 | // url_client, which sets autoLoginEnabled. 52 | api.addFiles('accounts_client.js', 'client'); 53 | api.addFiles('url_client.js', 'client'); 54 | api.addFiles('localstorage_token.js', 'client'); 55 | 56 | // These files instantiate the default Accounts instance on the server 57 | // and the client, so they must be evaluated last to ensure that the 58 | // prototypes have been fully populated. 59 | api.addFiles('globals_server.js', 'server'); 60 | api.addFiles('globals_client.js', 'client'); 61 | }); 62 | 63 | Package.onTest(function (api) { 64 | api.use([ 65 | 'accounts-base-pg-driver', 66 | 'simple:pg' 67 | ], ['server']); 68 | 69 | api.use([ 70 | 'accounts-base', 71 | 'tinytest', 72 | 'random', 73 | 'test-helpers', 74 | 'oauth-encryption', 75 | 'underscore', 76 | 'ddp' 77 | ]); 78 | 79 | api.addFiles('accounts_tests.js', 'server'); 80 | api.addFiles("accounts_url_tests.js", "client"); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/accounts-base/url_server.js: -------------------------------------------------------------------------------- 1 | // XXX These should probably not actually be public? 2 | 3 | AccountsServer.prototype.urls = { 4 | resetPassword: function (token) { 5 | return Meteor.absoluteUrl('#/reset-password/' + token); 6 | }, 7 | 8 | verifyEmail: function (token) { 9 | return Meteor.absoluteUrl('#/verify-email/' + token); 10 | }, 11 | 12 | enrollAccount: function (token) { 13 | return Meteor.absoluteUrl('#/enroll-account/' + token); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/accounts-password-pg-driver/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/packages/accounts-password-pg-driver/README.md -------------------------------------------------------------------------------- /packages/accounts-password-pg-driver/accounts-password-pg-driver.js: -------------------------------------------------------------------------------- 1 | AccountsDBClientPG.prototype.getUserByUsername = function getUserByUsername(username) { 2 | const userId = this._getUserIdByService("password", "username", username); 3 | return this.getUserById(userId); 4 | } 5 | 6 | AccountsDBClientPG.prototype.getUserByEmail = function getUserByEmail(email) { 7 | const userRow = PG.await(PG.knex 8 | .first("users.*") 9 | .from("users") 10 | .leftJoin("users_emails", "users.id", "users_emails.user_id") 11 | .where({ "users_emails.address": email }) 12 | ); 13 | 14 | if (userRow && userRow.id) { 15 | return this.getUserById(userRow.id); 16 | } else { 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/accounts-password-pg-driver/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'accounts-password-pg-driver', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.use([ 15 | 'simple:pg', 16 | 'accounts-base-pg-driver', 17 | 'ecmascript' 18 | ], "server"); 19 | 20 | api.versionsFrom('1.1.0.3'); 21 | api.addFiles('accounts-password-pg-driver.js', "server"); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/accounts-password/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/accounts-password/README.md: -------------------------------------------------------------------------------- 1 | # accounts-password 2 | 3 | A login service that enables secure password-based login. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. 4 | -------------------------------------------------------------------------------- /packages/accounts-password/email_templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Options to customize emails sent from the Accounts system. 3 | * @locus Server 4 | */ 5 | Accounts.emailTemplates = { 6 | from: "Meteor Accounts ", 7 | siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), 8 | 9 | resetPassword: { 10 | subject: function(user) { 11 | return "How to reset your password on " + Accounts.emailTemplates.siteName; 12 | }, 13 | text: function(user, url) { 14 | var greeting = (user.profile && user.profile.name) ? 15 | ("Hello " + user.profile.name + ",") : "Hello,"; 16 | return greeting + "\n" 17 | + "\n" 18 | + "To reset your password, simply click the link below.\n" 19 | + "\n" 20 | + url + "\n" 21 | + "\n" 22 | + "Thanks.\n"; 23 | } 24 | }, 25 | verifyEmail: { 26 | subject: function(user) { 27 | return "How to verify email address on " + Accounts.emailTemplates.siteName; 28 | }, 29 | text: function(user, url) { 30 | var greeting = (user.profile && user.profile.name) ? 31 | ("Hello " + user.profile.name + ",") : "Hello,"; 32 | return greeting + "\n" 33 | + "\n" 34 | + "To verify your account email, simply click the link below.\n" 35 | + "\n" 36 | + url + "\n" 37 | + "\n" 38 | + "Thanks.\n"; 39 | } 40 | }, 41 | enrollAccount: { 42 | subject: function(user) { 43 | return "An account has been created for you on " + Accounts.emailTemplates.siteName; 44 | }, 45 | text: function(user, url) { 46 | var greeting = (user.profile && user.profile.name) ? 47 | ("Hello " + user.profile.name + ",") : "Hello,"; 48 | return greeting + "\n" 49 | + "\n" 50 | + "To start using the service, simply click the link below.\n" 51 | + "\n" 52 | + url + "\n" 53 | + "\n" 54 | + "Thanks.\n"; 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /packages/accounts-password/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Password support for accounts", 3 | version: "1.1.2-rc.0" 4 | }); 5 | 6 | Package.onUse(function(api) { 7 | api.use('npm-bcrypt@=0.7.8_2'); 8 | 9 | api.use([ 10 | 'accounts-base', 11 | 'srp', 12 | 'sha', 13 | 'ejson', 14 | 'ddp' 15 | ], ['client', 'server']); 16 | 17 | // Export Accounts (etc) to packages using this one. 18 | api.imply('accounts-base', ['client', 'server']); 19 | 20 | api.use('email', ['server']); 21 | api.use('random', ['server']); 22 | api.use('check'); 23 | api.use('underscore'); 24 | 25 | api.addFiles('email_templates.js', 'server'); 26 | api.addFiles('password_server.js', 'server'); 27 | api.addFiles('password_client.js', 'client'); 28 | }); 29 | 30 | Package.onTest(function(api) { 31 | api.use(['accounts-password', 'tinytest', 'test-helpers', 'tracker', 32 | 'accounts-base', 'random', 'email', 'underscore', 'check', 33 | 'ddp']); 34 | api.addFiles('password_tests_setup.js', 'server'); 35 | api.addFiles('password_tests.js', ['client', 'server']); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/README.md: -------------------------------------------------------------------------------- 1 | # accounts-ui-unstyled 2 | 3 | A version of `accounts-ui` without the CSS, so that you can add your 4 | own styling. See the [`accounts-ui` 5 | README](https://atmospherejs.com/meteor/accounts-ui) and the 6 | Meteor Accounts [project page](https://www.meteor.com/accounts) for 7 | details. -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/accounts_ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Accounts UI 3 | * @namespace 4 | * @memberOf Accounts 5 | */ 6 | Accounts.ui = {}; 7 | 8 | Accounts.ui._options = { 9 | requestPermissions: {}, 10 | requestOfflineToken: {}, 11 | forceApprovalPrompt: {} 12 | }; 13 | 14 | // XXX refactor duplicated code in this function 15 | 16 | /** 17 | * @summary Configure the behavior of [`{{> loginButtons}}`](#accountsui). 18 | * @locus Client 19 | * @param {Object} options 20 | * @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service. 21 | * @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details. 22 | * @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. 23 | * @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default). 24 | */ 25 | Accounts.ui.config = function(options) { 26 | // validate options keys 27 | var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; 28 | _.each(_.keys(options), function (key) { 29 | if (!_.contains(VALID_KEYS, key)) 30 | throw new Error("Accounts.ui.config: Invalid key: " + key); 31 | }); 32 | 33 | // deal with `passwordSignupFields` 34 | if (options.passwordSignupFields) { 35 | if (_.contains([ 36 | "USERNAME_AND_EMAIL", 37 | "USERNAME_AND_OPTIONAL_EMAIL", 38 | "USERNAME_ONLY", 39 | "EMAIL_ONLY" 40 | ], options.passwordSignupFields)) { 41 | if (Accounts.ui._options.passwordSignupFields) 42 | throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); 43 | else 44 | Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; 45 | } else { 46 | throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); 47 | } 48 | } 49 | 50 | // deal with `requestPermissions` 51 | if (options.requestPermissions) { 52 | _.each(options.requestPermissions, function (scope, service) { 53 | if (Accounts.ui._options.requestPermissions[service]) { 54 | throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); 55 | } else if (!(scope instanceof Array)) { 56 | throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); 57 | } else { 58 | Accounts.ui._options.requestPermissions[service] = scope; 59 | } 60 | }); 61 | } 62 | 63 | // deal with `requestOfflineToken` 64 | if (options.requestOfflineToken) { 65 | _.each(options.requestOfflineToken, function (value, service) { 66 | if (service !== 'google') 67 | throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); 68 | 69 | if (Accounts.ui._options.requestOfflineToken[service]) { 70 | throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); 71 | } else { 72 | Accounts.ui._options.requestOfflineToken[service] = value; 73 | } 74 | }); 75 | } 76 | 77 | // deal with `forceApprovalPrompt` 78 | if (options.forceApprovalPrompt) { 79 | _.each(options.forceApprovalPrompt, function (value, service) { 80 | if (service !== 'google') 81 | throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); 82 | 83 | if (Accounts.ui._options.forceApprovalPrompt[service]) { 84 | throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); 85 | } else { 86 | Accounts.ui._options.forceApprovalPrompt[service] = value; 87 | } 88 | }); 89 | } 90 | }; 91 | 92 | passwordSignupFields = function () { 93 | return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; 94 | }; 95 | 96 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/accounts_ui_tests.js: -------------------------------------------------------------------------------- 1 | // XXX Most of the testing of accounts-ui is done manually, across 2 | // multiple browsers using examples/unfinished/accounts-ui-helper. We 3 | // should *definitely* automate this, but Tinytest is generally not 4 | // the right abstraction to use for this. 5 | 6 | 7 | // XXX it'd be cool to also test that the right thing happens if options 8 | // *are* validated, but Accounts.ui._options is global state which makes this hard 9 | // (impossible?) 10 | Tinytest.add('accounts-ui - config validates keys', function (test) { 11 | test.throws(function () { 12 | Accounts.ui.config({foo: "bar"}); 13 | }); 14 | 15 | test.throws(function () { 16 | Accounts.ui.config({passwordSignupFields: "not a valid option"}); 17 | }); 18 | 19 | test.throws(function () { 20 | Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); 21 | }); 22 | 23 | test.throws(function () { 24 | Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/login_buttons.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | 35 | 56 | 57 | 58 | 66 | 67 | 72 | 73 | 86 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/login_buttons.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // shared between dropdown and single mode 5 | Template.loginButtons.events({ 6 | 'click #login-buttons-logout': function() { 7 | Meteor.logout(function () { 8 | loginButtonsSession.closeDropdown(); 9 | }); 10 | } 11 | }); 12 | 13 | Template.registerHelper('loginButtons', function () { 14 | throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}"); 15 | }); 16 | 17 | // 18 | // helpers 19 | // 20 | 21 | displayName = function () { 22 | var user = Meteor.user(); 23 | if (!user) 24 | return ''; 25 | 26 | if (user.profile && user.profile.name) 27 | return user.profile.name; 28 | if (user.username) 29 | return user.username; 30 | if (user.emails && user.emails[0] && user.emails[0].address) 31 | return user.emails[0].address; 32 | 33 | return ''; 34 | }; 35 | 36 | // returns an array of the login services used by this app. each 37 | // element of the array is an object (eg {name: 'facebook'}), since 38 | // that makes it useful in combination with handlebars {{#each}}. 39 | // 40 | // don't cache the output of this function: if called during startup (before 41 | // oauth packages load) it might not include them all. 42 | // 43 | // NOTE: It is very important to have this return password last 44 | // because of the way we render the different providers in 45 | // login_buttons_dropdown.html 46 | getLoginServices = function () { 47 | var self = this; 48 | 49 | // First look for OAuth services. 50 | var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; 51 | 52 | // Be equally kind to all login services. This also preserves 53 | // backwards-compatibility. (But maybe order should be 54 | // configurable?) 55 | services.sort(); 56 | 57 | // Add password, if it's there; it must come last. 58 | if (hasPasswordService()) 59 | services.push('password'); 60 | 61 | return _.map(services, function(name) { 62 | return {name: name}; 63 | }); 64 | }; 65 | 66 | hasPasswordService = function () { 67 | return !!Package['accounts-password']; 68 | }; 69 | 70 | dropdown = function () { 71 | return hasPasswordService() || getLoginServices().length > 1; 72 | }; 73 | 74 | // XXX improve these. should this be in accounts-password instead? 75 | // 76 | // XXX these will become configurable, and will be validated on 77 | // the server as well. 78 | validateUsername = function (username) { 79 | if (username.length >= 3) { 80 | return true; 81 | } else { 82 | loginButtonsSession.errorMessage("Username must be at least 3 characters long"); 83 | return false; 84 | } 85 | }; 86 | validateEmail = function (email) { 87 | if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') 88 | return true; 89 | 90 | if (email.indexOf('@') !== -1) { 91 | return true; 92 | } else { 93 | loginButtonsSession.errorMessage("Invalid email"); 94 | return false; 95 | } 96 | }; 97 | validatePassword = function (password) { 98 | if (password.length >= 6) { 99 | return true; 100 | } else { 101 | loginButtonsSession.errorMessage("Password must be at least 6 characters long"); 102 | return false; 103 | } 104 | }; 105 | 106 | // 107 | // loginButtonLoggedOut template 108 | // 109 | 110 | Template._loginButtonsLoggedOut.helpers({ 111 | dropdown: dropdown, 112 | services: getLoginServices, 113 | singleService: function () { 114 | var services = getLoginServices(); 115 | if (services.length !== 1) 116 | throw new Error( 117 | "Shouldn't be rendering this template with more than one configured service"); 118 | return services[0]; 119 | }, 120 | configurationLoaded: function () { 121 | return true; //Accounts.loginServicesConfigured(); 122 | } 123 | }); 124 | 125 | 126 | // 127 | // loginButtonsLoggedIn template 128 | // 129 | 130 | // decide whether we should show a dropdown rather than a row of 131 | // buttons 132 | Template._loginButtonsLoggedIn.helpers({ 133 | dropdown: dropdown 134 | }); 135 | 136 | 137 | 138 | // 139 | // loginButtonsLoggedInSingleLogoutButton template 140 | // 141 | 142 | Template._loginButtonsLoggedInSingleLogoutButton.helpers({ 143 | displayName: displayName 144 | }); 145 | 146 | 147 | 148 | // 149 | // loginButtonsMessage template 150 | // 151 | 152 | Template._loginButtonsMessages.helpers({ 153 | errorMessage: function () { 154 | return loginButtonsSession.get('errorMessage'); 155 | } 156 | }); 157 | 158 | Template._loginButtonsMessages.helpers({ 159 | infoMessage: function () { 160 | return loginButtonsSession.get('infoMessage'); 161 | } 162 | }); 163 | 164 | 165 | // 166 | // loginButtonsLoggingInPadding template 167 | // 168 | 169 | Template._loginButtonsLoggingInPadding.helpers({ 170 | dropdown: dropdown 171 | }); 172 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/login_buttons_session.js: -------------------------------------------------------------------------------- 1 | var VALID_KEYS = [ 2 | 'dropdownVisible', 3 | 4 | // XXX consider replacing these with one key that has an enum for values. 5 | 'inSignupFlow', 6 | 'inForgotPasswordFlow', 7 | 'inChangePasswordFlow', 8 | 'inMessageOnlyFlow', 9 | 10 | 'errorMessage', 11 | 'infoMessage', 12 | 13 | // dialogs with messages (info and error) 14 | 'resetPasswordToken', 15 | 'enrollAccountToken', 16 | 'justVerifiedEmail', 17 | 'justResetPassword', 18 | 19 | 'configureLoginServiceDialogVisible', 20 | 'configureLoginServiceDialogServiceName', 21 | 'configureLoginServiceDialogSaveDisabled', 22 | 'configureOnDesktopVisible' 23 | ]; 24 | 25 | var validateKey = function (key) { 26 | if (!_.contains(VALID_KEYS, key)) 27 | throw new Error("Invalid key in loginButtonsSession: " + key); 28 | }; 29 | 30 | var KEY_PREFIX = "Meteor.loginButtons."; 31 | 32 | // XXX This should probably be package scope rather than exported 33 | // (there was even a comment to that effect here from before we had 34 | // namespacing) but accounts-ui-viewer uses it, so leave it as is for 35 | // now 36 | Accounts._loginButtonsSession = { 37 | set: function(key, value) { 38 | validateKey(key); 39 | if (_.contains(['errorMessage', 'infoMessage'], key)) 40 | throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage()."); 41 | 42 | this._set(key, value); 43 | }, 44 | 45 | _set: function(key, value) { 46 | Session.set(KEY_PREFIX + key, value); 47 | }, 48 | 49 | get: function(key) { 50 | validateKey(key); 51 | return Session.get(KEY_PREFIX + key); 52 | }, 53 | 54 | closeDropdown: function () { 55 | this.set('inSignupFlow', false); 56 | this.set('inForgotPasswordFlow', false); 57 | this.set('inChangePasswordFlow', false); 58 | this.set('inMessageOnlyFlow', false); 59 | this.set('dropdownVisible', false); 60 | this.resetMessages(); 61 | }, 62 | 63 | infoMessage: function(message) { 64 | this._set("errorMessage", null); 65 | this._set("infoMessage", message); 66 | this.ensureMessageVisible(); 67 | }, 68 | 69 | errorMessage: function(message) { 70 | this._set("errorMessage", message); 71 | this._set("infoMessage", null); 72 | this.ensureMessageVisible(); 73 | }, 74 | 75 | // is there a visible dialog that shows messages (info and error) 76 | isMessageDialogVisible: function () { 77 | return this.get('resetPasswordToken') || 78 | this.get('enrollAccountToken') || 79 | this.get('justVerifiedEmail'); 80 | }, 81 | 82 | // ensure that somethings displaying a message (info or error) is 83 | // visible. if a dialog with messages is open, do nothing; 84 | // otherwise open the dropdown. 85 | // 86 | // notably this doesn't matter when only displaying a single login 87 | // button since then we have an explicit message dialog 88 | // (_loginButtonsMessageDialog), and dropdownVisible is ignored in 89 | // this case. 90 | ensureMessageVisible: function () { 91 | if (!this.isMessageDialogVisible()) 92 | this.set("dropdownVisible", true); 93 | }, 94 | 95 | resetMessages: function () { 96 | this._set("errorMessage", null); 97 | this._set("infoMessage", null); 98 | }, 99 | 100 | configureService: function (name) { 101 | if (Meteor.isCordova) { 102 | this.set('configureOnDesktopVisible', true); 103 | } else { 104 | this.set('configureLoginServiceDialogVisible', true); 105 | this.set('configureLoginServiceDialogServiceName', name); 106 | this.set('configureLoginServiceDialogSaveDisabled', true); 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/login_buttons_single.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/login_buttons_single.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | 5 | var loginResultCallback = function (serviceName, err) { 6 | if (!err) { 7 | loginButtonsSession.closeDropdown(); 8 | } else if (err instanceof Accounts.LoginCancelledError) { 9 | // do nothing 10 | } else if (err instanceof ServiceConfiguration.ConfigError) { 11 | loginButtonsSession.configureService(serviceName); 12 | } else { 13 | loginButtonsSession.errorMessage(err.reason || "Unknown error"); 14 | } 15 | }; 16 | 17 | 18 | // In the login redirect flow, we'll have the result of the login 19 | // attempt at page load time when we're redirected back to the 20 | // application. Register a callback to update the UI (i.e. to close 21 | // the dialog on a successful login or display the error on a failed 22 | // login). 23 | // 24 | Accounts.onPageLoadLogin(function (attemptInfo) { 25 | // Ignore if we have a left over login attempt for a service that is no longer registered. 26 | if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type)) 27 | loginResultCallback(attemptInfo.type, attemptInfo.error); 28 | }); 29 | 30 | 31 | Template._loginButtonsLoggedOutSingleLoginButton.events({ 32 | 'click .login-button': function () { 33 | var serviceName = this.name; 34 | loginButtonsSession.resetMessages(); 35 | 36 | // XXX Service providers should be able to specify their 37 | // `Meteor.loginWithX` method name. 38 | var loginWithService = Meteor["loginWith" + 39 | (serviceName === 'meteor-developer' ? 40 | 'MeteorDeveloperAccount' : 41 | capitalize(serviceName))]; 42 | 43 | var options = {}; // use default scope unless specified 44 | if (Accounts.ui._options.requestPermissions[serviceName]) 45 | options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; 46 | if (Accounts.ui._options.requestOfflineToken[serviceName]) 47 | options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; 48 | if (Accounts.ui._options.forceApprovalPrompt[serviceName]) 49 | options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; 50 | 51 | loginWithService(options, function (err) { 52 | loginResultCallback(serviceName, err); 53 | }); 54 | } 55 | }); 56 | 57 | Template._loginButtonsLoggedOutSingleLoginButton.helpers({ 58 | configured: function () { 59 | return !!ServiceConfiguration.configurations.findOne({service: this.name}); 60 | }, 61 | capitalizedName: function () { 62 | if (this.name === 'github') 63 | // XXX we should allow service packages to set their capitalized name 64 | return 'GitHub'; 65 | else if (this.name === 'meteor-developer') 66 | return 'Meteor'; 67 | else 68 | return capitalize(this.name); 69 | } 70 | }); 71 | 72 | // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js 73 | var capitalize = function(str){ 74 | str = str == null ? '' : String(str); 75 | return str.charAt(0).toUpperCase() + str.slice(1); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/accounts-ui-unstyled/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Unstyled version of login widgets", 3 | version: "1.1.8-rc.0" 4 | }); 5 | 6 | Package.onUse(function (api) { 7 | api.use(['tracker', 'service-configuration', 'accounts-base', 8 | 'underscore', 'templating', 'session', 'jquery'], 'client'); 9 | // Export Accounts (etc) to packages using this one. 10 | api.imply('accounts-base', ['client', 'server']); 11 | 12 | // Allow us to call Accounts.oauth.serviceNames, if there are any OAuth 13 | // services. 14 | api.use('accounts-oauth', {weak: true}); 15 | // Allow us to directly test if accounts-password (which doesn't use 16 | // Accounts.oauth.registerService) exists. 17 | api.use('accounts-password', {weak: true}); 18 | 19 | api.addFiles([ 20 | 'accounts_ui.js', 21 | 22 | 'login_buttons.html', 23 | 'login_buttons_single.html', 24 | 'login_buttons_dropdown.html', 25 | 'login_buttons_dialogs.html', 26 | 27 | 'login_buttons_session.js', 28 | 29 | 'login_buttons.js', 30 | 'login_buttons_single.js', 31 | 'login_buttons_dropdown.js', 32 | 'login_buttons_dialogs.js'], 'client'); 33 | 34 | // The less source defining the default style for accounts-ui. Just adding 35 | // this package doesn't actually apply these styles; they need to be 36 | // `@import`ed from some non-import less file. The accounts-ui package does 37 | // that for you, or you can do it in your app. 38 | api.use('less'); 39 | api.addFiles('login_buttons.import.less'); 40 | }); 41 | 42 | Package.onTest(function (api) { 43 | api.use('accounts-ui-unstyled'); 44 | api.use('tinytest'); 45 | api.addFiles('accounts_ui_tests.js', 'client'); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/bookshelf/.gitignore: -------------------------------------------------------------------------------- 1 | *.browserify.js.cached 2 | *.browserify.js.map 3 | -------------------------------------------------------------------------------- /packages/bookshelf/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/bookshelf/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /packages/bookshelf/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/packages/bookshelf/README.md -------------------------------------------------------------------------------- /packages/bookshelf/bookshelf-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | Tinytest.add('example', function (test) { 4 | test.equal(true, true); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/bookshelf/bookshelf.browserify.js: -------------------------------------------------------------------------------- 1 | BrowserifyBookshelf = require('bookshelf'); 2 | -------------------------------------------------------------------------------- /packages/bookshelf/bookshelf.browserify.options.json: -------------------------------------------------------------------------------- 1 | { 2 | "transforms": { 3 | "exposify": { 4 | "global": true, 5 | "expose": { 6 | "bluebird": "__Sync_BlueBird", 7 | "lodash": "Package.underscore._", 8 | "lodash/object/assign": "Package.underscore._.extend" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/bookshelf/bookshelf.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Bookshelf = Npm.require('bookshelf'); 3 | } else { 4 | Bookshelf = BrowserifyBookshelf; 5 | } 6 | 7 | // Fix incompatibilities of lodash and underscore here 8 | // Pretty hacky, but if we don't want to load another copy of '_', we 9 | // need to keep them in sort of sync 10 | const origSome = _.some; 11 | _.some = function (list, predicate, context) { 12 | if (typeof predicate === 'function') 13 | return origSome.call(this, list, predicate, context); 14 | if (typeof predicate === 'string') 15 | return origSome(list, function (x) { 16 | return !!Meteor._get(x, ...predicate.split('.')); 17 | }, context); 18 | return !!_.findWhere(list, predicate, context); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/bookshelf/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'simple:bookshelf', 3 | version: '0.0.1', 4 | documentation: null 5 | }); 6 | 7 | Npm.depends({ 8 | bookshelf: '0.8.1', 9 | knex: '0.8.6', 10 | exposify: '0.4.3', 11 | bluebird: '2.9.34' 12 | }); 13 | 14 | Package.onUse(function(api) { 15 | api.versionsFrom('1.1.0.3'); 16 | api.use('ecmascript'); 17 | api.use('underscore'); 18 | 19 | api.use('cosmos:browserify@0.5.0'); 20 | api.addFiles('sync-promise.browserify.js', 'client'); 21 | api.addFiles('bookshelf.browserify.js', 'client'); 22 | api.addFiles('bookshelf.browserify.options.json', 'client'); 23 | 24 | api.addFiles('knex.js'); 25 | api.addFiles('bookshelf.js'); 26 | 27 | api.export(['Bookshelf', 'Knex']); 28 | }); 29 | 30 | Package.onTest(function(api) { 31 | api.use('ecmascript'); 32 | api.use('tinytest'); 33 | api.use('simple:bookshelf'); 34 | api.addFiles('knex_tests.js', 'client'); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/bookshelf/sync-promise.browserify.js: -------------------------------------------------------------------------------- 1 | // This file compiles the 'bluebird' npm module for the browser that 2 | // has a custom scheduler that always tries to finish non-async tasks 3 | // synchronously before the call returns. 4 | // This is important to be able to write a knex driver on top of 5 | // Minimongo, since we know that all calls to Minimongo return 6 | // synchronously. 7 | 8 | var Promise = require("bluebird"); 9 | var callbackQueueStack = []; 10 | 11 | Promise.setScheduler(function(callback) { 12 | if (callbackQueueStack.length > 0) { 13 | callbackQueueStack[callbackQueueStack.length - 1].push(callback); 14 | } else if (typeof process === "object" && 15 | typeof process.nextTick === "function") { 16 | process.nextTick(callback); 17 | } else { 18 | Meteor.setTimeout(callback, 0); 19 | } 20 | }); 21 | 22 | // use this function to wrap the methods that normally return a 23 | // Promise and make it synchronous 24 | Promise.synchronize = function(fn) { 25 | return function wrapper() { 26 | var result = fn.apply(this, arguments); 27 | if (Promise.is(result)) { 28 | return Promise.await(result); 29 | } else { 30 | return result; 31 | } 32 | }; 33 | }; 34 | 35 | Promise.await = function (promise) { 36 | var queue = []; 37 | var gotResult = false; 38 | var result; 39 | 40 | callbackQueueStack.push(queue); 41 | 42 | try { 43 | promise.done(function(value) { 44 | result = value; 45 | gotResult = true; 46 | }); 47 | } finally { 48 | for (var i = 0; i < queue.length; ++i) { 49 | try { 50 | (0, queue[i])(); 51 | } finally { 52 | continue; 53 | } 54 | } 55 | callbackQueueStack.pop(); 56 | } 57 | 58 | if (! gotResult) { 59 | throw new Error( 60 | "Promise.synchronize could not fulfill all promises synchronously" 61 | ); 62 | } 63 | 64 | return result; 65 | }; 66 | 67 | window.__Sync_BlueBird = Promise; 68 | -------------------------------------------------------------------------------- /packages/pg/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/pg/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /packages/pg/.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "murmurhash-js": { 4 | "version": "1.0.0" 5 | }, 6 | "pg": { 7 | "version": "4.4.1", 8 | "dependencies": { 9 | "buffer-writer": { 10 | "version": "1.0.0" 11 | }, 12 | "generic-pool": { 13 | "version": "2.1.1" 14 | }, 15 | "packet-reader": { 16 | "version": "0.2.0" 17 | }, 18 | "pg-connection-string": { 19 | "version": "0.1.3" 20 | }, 21 | "pg-types": { 22 | "version": "1.10.0", 23 | "dependencies": { 24 | "ap": { 25 | "version": "0.2.0" 26 | }, 27 | "postgres-array": { 28 | "version": "1.0.0" 29 | }, 30 | "postgres-bytea": { 31 | "version": "1.0.0" 32 | }, 33 | "postgres-date": { 34 | "version": "1.0.0" 35 | }, 36 | "postgres-interval": { 37 | "version": "1.0.0", 38 | "dependencies": { 39 | "xtend": { 40 | "version": "4.0.0" 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "pgpass": { 47 | "version": "0.0.3", 48 | "dependencies": { 49 | "split": { 50 | "version": "0.3.3", 51 | "dependencies": { 52 | "through": { 53 | "version": "2.3.8" 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "semver": { 60 | "version": "4.3.6" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/pg/README.md: -------------------------------------------------------------------------------- 1 | This package provides an interface to access Postgresql database from the 2 | application server, as well publishing data to the client and maintaining the 3 | client-side cache. 4 | 5 | 6 | 7 | ## Observe Driver 8 | 9 | The current observe driver is a rewritten and modified version of the driver 10 | written [by Ben Green](https://github.com/numtel/pg-live-select). 11 | 12 | The driver currently only works with Postresql and here is how it works (polling 13 | driver): 14 | 15 | - Define a trigger on every table we are interested in 16 | - When a write occurs, the trigger broadcasts a notification with the payload 17 | representing the new value of the row (there could be multiple notifications, 18 | due to PG's limit on the length) 19 | - The application servers listen to these notifications and based on the payload 20 | can decide to poll or not to poll (based on the invalidation callbacks) 21 | - If the application decides to poll, it sends a list of known hashes of the 22 | rows into Postgresql (if `poll.sql` is used) and the PG returns only rows that 23 | has changed 24 | * An alternative method, `poll-n-diff.sql` diffs the changes against a 25 | temporary table purely inside the Postgresql server 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/pg/collection-client.js: -------------------------------------------------------------------------------- 1 | // Initialize Knex on the client with no connection 2 | PG.knex = Knex(null); 3 | 4 | // the prototype of the chained query builder from knex 5 | const QBProto = Knex.QueryBuilder.prototype; 6 | 7 | PG.knex.raw = function raw() { 8 | throw new Error(`PG: raw not on client`, `Can't use raw SQL queries on the client.`); 9 | } 10 | 11 | // Run the query against minimongo and return exactly what minimongo would have 12 | // returned, don't call fetch on it. 13 | QBProto._runNotFetch = function _runNotFetch() { 14 | const { 15 | collection, 16 | method, 17 | selector, 18 | modifier, 19 | projection, 20 | sort, 21 | limit, 22 | skip 23 | } = this.toMongoQuery(); 24 | 25 | const options = { 26 | sort, limit, skip, 27 | fields: projection 28 | }; 29 | 30 | if (! collection) { 31 | throw new Error(`PG: no table`, `Specify the table to query. E.g.: PG.knex('table')...`); 32 | } 33 | 34 | // run this query against local minimongo 35 | const minimongo = Mongo.Collection.get(collection); 36 | const args = []; 37 | 38 | if (! minimongo) { 39 | throw new Error(`PG: table not on client`, `Specified table '${collection}' is not registered on the Client.`); 40 | } 41 | 42 | if (method === 'find') { 43 | args.push(selector, options); 44 | } 45 | if (method === 'insert') { 46 | args.push(modifier); 47 | } 48 | if (method === 'update') { 49 | args.push(selector, modifier, {multi: true}); 50 | } 51 | if (method === 'remove') { 52 | args.push(selector); 53 | } 54 | 55 | // XXX will not work for things like "insert V into T returning *" 56 | const ret = minimongo[method](...args); 57 | 58 | return ret; 59 | }; 60 | 61 | // A way for the Knex queries to run synchronously on the client without 62 | // promises 63 | QBProto.run = function () { 64 | const ret = this._runNotFetch(); 65 | 66 | // Call fetch if this is a mongo cursor because this should return an array 67 | return _.isFunction(ret.fetch) ? ret.fetch() : ret; 68 | }; 69 | 70 | QBProto._applyMethodOnCursor = function _applyMethodOnCursor(methodName, args) { 71 | const ret = this._runNotFetch(); 72 | 73 | // Call methodName, but error if this is not actually a read query 74 | if (_.isFunction(ret[methodName])) { 75 | return ret[methodName].apply(ret, args); 76 | } else { 77 | throw new Error(`PG: not select`, `Can only call ${methodName}() on select queries.`); 78 | } 79 | }; 80 | 81 | // Minimongo cursor methods 82 | [ 83 | 'fetch', 84 | 'observe', 85 | 'observeChanges', 86 | 'forEach', 87 | 'map', 88 | 'count' 89 | ].forEach((methodName) => { 90 | QBProto[methodName] = function () { 91 | return this._applyMethodOnCursor(methodName, arguments); 92 | }; 93 | }); 94 | 95 | PG.Model = class Model { 96 | constructor(doc) { 97 | _.extend(this, doc); 98 | } 99 | } 100 | 101 | // XXX will only work with the methods of Bookshelf as they use the 102 | // version of BlueBird that we supplied over a transform with exposify. 103 | PG.await = window.__Sync_BlueBird.await; 104 | -------------------------------------------------------------------------------- /packages/pg/collection-server.js: -------------------------------------------------------------------------------- 1 | // hack so Knex from simple:bookshelf doesn't try to load 'pg' 2 | Knex.Client.prototype.initializeDriver = function () { 3 | this.driver = PG._npmModule; 4 | }; 5 | 6 | // Initialize Knex with a connection to the server 7 | PG.knex = Knex({ 8 | client: 'pg', 9 | connection: PG.defaultConnectionUrl 10 | }); 11 | 12 | // the prototype of the chained query builder from knex 13 | const QBProto = Knex.Client.prototype.QueryBuilder.prototype; 14 | 15 | // Apparently you need this to publish multiple cursors... 16 | QBProto._getCollectionName = function () { 17 | const tableName = this._publishAs ? this._publishAs : this._single.table; 18 | return tableName; 19 | }; 20 | 21 | QBProto._publishCursor = function (sub) { 22 | const queryStr = this.toString(); 23 | const tableName = this._getCollectionName(); 24 | return new PG.Query(queryStr, tableName)._publishCursor(sub); 25 | }; 26 | 27 | QBProto.observe = function (...args) { 28 | const queryStr = this.toString(); 29 | const tableName = this._getCollectionName(); 30 | const pgQuery = new PG.Query(queryStr, tableName); 31 | return pgQuery.observe.apply(pgQuery, args); 32 | }; 33 | 34 | 35 | const oldRaw = PG.knex.raw; 36 | PG.knex.raw = function () { 37 | const ret = oldRaw.apply(PG.knex, arguments); 38 | 39 | ret.publishAs = (tableName) => { 40 | ret._publishAs = tableName; 41 | return ret; 42 | }; 43 | 44 | ret._publishCursor = (sub) => { 45 | if (! ret._publishAs) { 46 | throw new Error(`PG: set table name`, `Need to set table name with .publishAs() if publishing a raw query.`); 47 | } 48 | 49 | return new PG.Query(ret.toString(), ret._publishAs)._publishCursor(sub); 50 | }; 51 | 52 | return ret; 53 | } 54 | 55 | // a way for the Knex queries to run using Fibers on the server 56 | QBProto.run = function run() { 57 | const result = PG.await(this); 58 | const table = this._single.table; 59 | const method = this._method; 60 | 61 | // XXX weird use of global vars 62 | const fence = DDPServer._CurrentWriteFence.get(); 63 | if (fence) { 64 | if (method !== 'select') { 65 | PG._livePg.appendPendingWrite(table, fence.beginWrite()); 66 | } 67 | } 68 | 69 | return result; 70 | }; 71 | 72 | QBProto.fetch = function fetch() { 73 | if (this._method === 'select') { 74 | return this.run(); 75 | } else { 76 | throw new Error(`PG: fetch must be select`, `Can only call fetch/fetchOne/fetchValue on select queries.`); 77 | } 78 | } 79 | 80 | QBProto.publishAs = function publishAs(tableName) { 81 | this._publishAs = tableName; 82 | }; 83 | 84 | PG.Model = function modelError() { 85 | throw new Error(`PG.Model not on server`, `PG.Model not implemented for the server.`); 86 | } 87 | 88 | const Future = Npm.require('fibers/future'); 89 | 90 | function awaitPromise(promise) { 91 | var f = new Future(); 92 | promise.then( 93 | Meteor.bindEnvironment(res => f.return(res)), 94 | Meteor.bindEnvironment(err => f.throw(err)) 95 | ); 96 | 97 | return f.wait(); 98 | } 99 | 100 | PG.await = awaitPromise; 101 | -------------------------------------------------------------------------------- /packages/pg/collection.js: -------------------------------------------------------------------------------- 1 | const bookshelf = Bookshelf(PG.knex); 2 | const origModelForge = bookshelf.Model.forge; 3 | bookshelf.Model.forge = function () { 4 | const ret = origModelForge.apply(this, arguments); 5 | // monkey-patch forge, so we can make simple queries 6 | // originated from Model publishable 7 | ret._publishCursor = function (sub) { 8 | return this._knex._publishCursor(sub); 9 | }; 10 | return ret; 11 | }; 12 | 13 | PG.Table = class Table { 14 | constructor(tableName, options = {}) { 15 | this.knex = function () { 16 | return PG.knex(tableName) 17 | }; 18 | 19 | // Should be applied via transform 20 | // Not sure how to do this on the server 21 | this.modelClass = options.modelClass; 22 | 23 | if (Meteor.isClient) { 24 | const minimongoOptions = {}; 25 | if (this.modelClass) { 26 | minimongoOptions.transform = (doc) => { 27 | // Maybe we can make this more efficient later, now that we know the 28 | // transform is specific 29 | return new this.modelClass(doc); 30 | }; 31 | } 32 | 33 | // register a minimongo store for this table 34 | this.minimongo = new Mongo.Collection(tableName, minimongoOptions); 35 | } else { 36 | const exists = PG.await(PG.knex.schema.hasTable(tableName)); 37 | 38 | if (exists) { 39 | // autopublish (options._preventAutopublish for possible future use) 40 | if (Package.autopublish && !options._preventAutopublish) { 41 | Meteor.publish(null, () => { 42 | return this.knex(); 43 | }, { 44 | is_auto: true 45 | }); 46 | } 47 | } else { 48 | throw new Error(`PG: no such table`, `Table '${tableName}' doesn't exist. Please create it in a migration.`); 49 | } 50 | } 51 | } 52 | } 53 | 54 | // Get the prototype of the query builder 55 | const QBProto = Meteor.isClient ? 56 | Knex.QueryBuilder.prototype : 57 | Knex.Client.prototype.QueryBuilder.prototype; 58 | 59 | // Addresses https://github.com/meteor/postgres-packages/issues/9 60 | // Fetches the first row of a select query. 61 | QBProto.fetchOne = function fetchOne() { 62 | const rows = QBProto.fetch.call(this); 63 | if (rows.length === 0) { 64 | return; // It may not be ready yet, so return undefined 65 | } else { 66 | return rows[0]; 67 | } 68 | }; 69 | 70 | // Addresses https://github.com/meteor/postgres-packages/issues/9 71 | // If one column is returned by the query fetchValue() gets it. 72 | // If more than one column is returned, use fetchValue(columnName) to get it. 73 | QBProto.fetchValue = function fetchValue(column) { 74 | const row = QBProto.fetchOne.call(this); 75 | if (_.isUndefined(row)) { 76 | return; // Early return if undefined. 77 | } 78 | if (_.isUndefined(column)) { // If no column was requested ... 79 | const keys = Object.keys(row); 80 | if (keys.length === 1) { // we only expect one in the response. 81 | return row[keys[0]]; 82 | } else { 83 | throw new Error(`PG: fetchValue too many columns`, `fetchValue(): query returned more than one column.`); 84 | } 85 | } else if (_.isString(column)) { // If a column was requested ... 86 | if (_.isUndefined(row[column])) { // we expect it to be there 87 | return; // It may not be ready yet, so return undefined. 88 | } else { 89 | return row[column]; // and we return it if it is. 90 | } 91 | } else { 92 | throw new Error(`PG: fetchValue parameter not string`, `fetchValue(column): column must be a string.`); 93 | } 94 | }; 95 | 96 | // Proxy all query builder methods; so when you type MyTable.select() it does 97 | // MyTable.knex().select() 98 | const methods = Object.getOwnPropertyNames(QBProto); 99 | methods.forEach((methodName) => { 100 | PG.Table.prototype[methodName] = function (...args) { 101 | const qb = this.knex(); 102 | return qb[methodName].apply(qb, args); 103 | }; 104 | }); 105 | -------------------------------------------------------------------------------- /packages/pg/observe-driver/poll-n-diff.sql: -------------------------------------------------------------------------------- 1 | /* Creates a temporary table if such doesn't exists to store the 2 | * hashes of the previously computed result. 3 | * Returns the changed rows with extra columns 'hash' and 'delta_id'. 4 | * If the query fields are all set to null, the row was removed from the set. 5 | * 6 | * Arguments: query, queryName, deltaIdType (int or string are common) 7 | */ 8 | CREATE TEMP TABLE IF NOT EXISTS $$queryName$$_cache (id $$deltaIdType$$, _hash TEXT); 9 | WITH 10 | res as ($$query$$), 11 | vals as (SELECT *, MD5(CAST(ROW_TO_JSON(res.*) AS TEXT)) AS hash FROM res), 12 | updated as ( 13 | UPDATE $$queryName$$_cache 14 | SET _hash = vals.hash 15 | FROM vals 16 | WHERE _hash <> hash and $$queryName$$_cache.id = vals.id 17 | RETURNING vals.id 18 | ), 19 | inserted as ( 20 | INSERT INTO $$queryName$$_cache(id, _hash) 21 | ( 22 | SELECT id, hash 23 | FROM vals 24 | WHERE id NOT IN (SELECT id from $$queryName$$_cache) 25 | ) 26 | RETURNING id 27 | ), 28 | deleted as ( 29 | DELETE FROM $$queryName$$_cache 30 | WHERE id NOT IN (SELECT id from vals) 31 | RETURNING id 32 | ) 33 | 34 | SELECT deltas.id AS delta_id, vals.* FROM ( 35 | SELECT * FROM updated UNION 36 | SELECT * FROM inserted UNION 37 | SELECT * FROM deleted 38 | ) AS deltas LEFT OUTER JOIN vals ON deltas.id = vals.id; 39 | -------------------------------------------------------------------------------- /packages/pg/observe-driver/poll.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Template for refreshing a result set, only returning unknown rows 3 | * Accepts 2 arguments: 4 | * query: original query string 5 | * hashParam: count of params in original query + 1. This is used to 6 | * pass hashes as the last argument. Since the original $$query$$ can 7 | * expand to something that accepts arguments (contain $1, $2, etc), 8 | * it is important to expand this to $3 or whatever the last argument 9 | * index is. 10 | */ 11 | WITH 12 | query_result AS ($$query$$), 13 | result_with_hashes AS ( 14 | SELECT 15 | query_result.*, 16 | MD5(CAST(ROW_TO_JSON(query_result.*) AS TEXT)) AS _hash 17 | FROM query_result 18 | ), 19 | 20 | new_and_changed AS ( 21 | SELECT result_with_hashes.* 22 | FROM result_with_hashes 23 | WHERE NOT (_hash = ANY ( 24 | /* NOTE the tripple dollar on the left */ 25 | $$$hashParam$$)) 26 | ), 27 | removed AS ( 28 | SELECT * 29 | FROM (VALUES $$stringifiedHashesList$$) AS t(_hash) 30 | WHERE NOT (t._hash IN (SELECT _hash FROM result_with_hashes)) 31 | ) 32 | 33 | SELECT NULL AS removed_hash, * FROM new_and_changed UNION ALL 34 | SELECT removed._hash AS removed_hash, result_with_hashes.* FROM removed LEFT JOIN result_with_hashes ON removed._hash = result_with_hashes._hash 35 | -------------------------------------------------------------------------------- /packages/pg/observe-driver/setup-triggers.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Template for trigger function to send row changes over notification 3 | * Accepts 2 arguments: 4 | * triggerFunction: name of function to create/replace 5 | * channel: NOTIFY channel on which to broadcast changes 6 | */ 7 | CREATE OR REPLACE FUNCTION "$$triggerFunction$$"() RETURNS trigger AS $$ 8 | DECLARE 9 | row_data RECORD; 10 | full_msg TEXT; 11 | full_len INT; 12 | cur_page INT; 13 | page_count INT; 14 | msg_hash TEXT; 15 | BEGIN 16 | IF (TG_OP = 'INSERT') THEN 17 | SELECT 18 | TG_TABLE_NAME AS table, 19 | TG_OP AS op, 20 | json_agg(NEW) AS data 21 | INTO row_data; 22 | ELSIF (TG_OP = 'DELETE') THEN 23 | SELECT 24 | TG_TABLE_NAME AS table, 25 | TG_OP AS op, 26 | json_agg(OLD) AS data 27 | INTO row_data; 28 | ELSIF (TG_OP = 'UPDATE') THEN 29 | SELECT 30 | TG_TABLE_NAME AS table, 31 | TG_OP AS op, 32 | json_agg(NEW) AS new_data, 33 | json_agg(OLD) AS old_data 34 | INTO row_data; 35 | END IF; 36 | 37 | SELECT row_to_json(row_data)::TEXT INTO full_msg; 38 | SELECT char_length(full_msg) INTO full_len; 39 | SELECT (full_len / 7950) + 1 INTO page_count; 40 | SELECT md5(full_msg) INTO msg_hash; 41 | 42 | FOR cur_page IN 1..page_count LOOP 43 | PERFORM pg_notify('$$channel$$', 44 | msg_hash || ':' || page_count || ':' || cur_page || ':' || 45 | substr(full_msg, ((cur_page - 1) * 7950) + 1, 7950) 46 | ); 47 | END LOOP; 48 | RETURN NULL; 49 | END; 50 | $$ LANGUAGE plpgsql; 51 | -------------------------------------------------------------------------------- /packages/pg/observe-driver/tests.js: -------------------------------------------------------------------------------- 1 | const pg = Npm.require('pg'); 2 | 3 | const connectionUrl = process.env.POSTGRES_URL || 'postgres://127.0.0.1/postgres'; 4 | 5 | function runInFence(f) { 6 | if (Meteor.isClient) { 7 | throw new Error(`PG: fence not in client`, `client code cannot run in a fence`); 8 | } 9 | 10 | const fence = new DDPServer._WriteFence; 11 | DDPServer._CurrentWriteFence.withValue(fence, f); 12 | fence.armAndWait(); 13 | } 14 | 15 | function queryWithWriteFenceTable(db, table) { 16 | const querySync = db.client.querySync.bind(db.client); 17 | return function (query, params) { 18 | const ret = querySync(query, params); 19 | const fence = DDPServer._CurrentWriteFence.get(); 20 | if (fence) { 21 | db.appendPendingWrite(table, fence.beginWrite()); 22 | } 23 | 24 | return ret; 25 | }; 26 | } 27 | 28 | Tinytest.add('pg - polling-driver - basic', (test) => { 29 | const db = new PgLiveQuery({connectionUrl}); 30 | const run = queryWithWriteFenceTable(db, 'employees'); 31 | 32 | run('DROP TABLE IF EXISTS employees;'); 33 | run('CREATE TABLE employees (id serial primary key, name text);'); 34 | run('INSERT INTO employees(name) VALUES (\'slava\');'); 35 | run('INSERT INTO employees(name) VALUES (\'sashko\');'); 36 | 37 | const notifs = []; 38 | db.select('SELECT * FROM employees', [], {}, { 39 | added(newVal) { 40 | notifs.push(['added', newVal]); 41 | }, 42 | changed(newVal, oldVal) { 43 | notifs.push(['changed', newVal, oldVal]); 44 | }, 45 | removed(oldVal) { 46 | notifs.push(['removed', oldVal]); 47 | } 48 | }); 49 | 50 | test.equal(notifs.shift(), ['added', {id: 1, name: 'slava'}]); 51 | test.equal(notifs.shift(), ['added', {id: 2, name: 'sashko'}]); 52 | 53 | runInFence(function () { 54 | run('UPDATE employees SET name=\'avital\' where id=2'); 55 | }); 56 | test.equal(notifs.shift(), ['changed', {id: 2, name: 'avital'}, {id: 2, name: 'sashko'}]); 57 | 58 | db.stop(); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/pg/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'simple:pg', 3 | version: '0.0.2', 4 | summary: 'XXX it almost does what you would expect', 5 | documentation: null 6 | }); 7 | 8 | Npm.depends({ 9 | 'pg': '4.4.1', 10 | 'murmurhash-js': '1.0.0' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.1.0.3'); 15 | 16 | // PACKAGE DEPENDENCIES 17 | api.use([ 18 | 'underscore', 19 | 'ecmascript', 20 | 'simple:bookshelf' 21 | ]); 22 | 23 | api.use([ 24 | 'random', 25 | 'ejson', 26 | 'jsx' 27 | ], 'server'); 28 | 29 | api.use([ 30 | 'dburles:mongo-collection-instances', 31 | 'mongo' 32 | ], 'client'); 33 | 34 | // ADD FILES 35 | api.addFiles([ 36 | 'pre.js', 37 | 'transaction.js' 38 | ]); 39 | 40 | // observe driver 41 | api.use(['underscore', 'ddp-server'], 'server'); 42 | api.addFiles(['observe-driver/polling-driver.js'], 'server'); 43 | 44 | if (api.addAssets) { 45 | api.addAssets([ 46 | 'observe-driver/setup-triggers.sql', 47 | 'observe-driver/poll-n-diff.sql', 48 | 'observe-driver/poll.sql' 49 | ], 'server'); 50 | } else { 51 | api.addFiles([ 52 | 'observe-driver/setup-triggers.sql', 53 | 'observe-driver/poll-n-diff.sql', 54 | 'observe-driver/poll.sql' 55 | ], 'server', { isAsset: true }); 56 | } 57 | 58 | api.addFiles([ 59 | 'pg.js', 60 | 'collection-server.js' 61 | ], 'server'); 62 | 63 | api.addFiles([ 64 | 'collection-client.js' 65 | ], 'client'); 66 | 67 | // Needs to be loaded last, uses setup from client or server 68 | api.addFiles([ 69 | 'collection.js' 70 | ]); 71 | 72 | // EXPORT 73 | api.export('PG'); 74 | api.export('PgLiveQuery'); 75 | }); 76 | 77 | Package.onTest(function(api) { 78 | api.use(['tinytest', 'promise', 'simple:pg', 'ecmascript']); 79 | api.addFiles('pg-tests.js'); 80 | api.addFiles('pg-server-tests.js', 'server'); 81 | 82 | api.use(['ddp-server'], 'server'); 83 | api.addFiles(['observe-driver/tests.js'], 'server'); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/pg/pg-server-tests.js: -------------------------------------------------------------------------------- 1 | let Table = null; // All being well we'll overwrite this later 2 | const tablename = 'testing'; // Pick a sensible (?) table name 3 | 4 | Tinytest.add('pg - schema builder - create testing table', (test) => { 5 | function dropTable() { // Set up a promised drop table 6 | let promise = PG.knex.schema.dropTableIfExists(tablename); 7 | return Promise.await(promise); 8 | }; 9 | 10 | function createTable() { // Set up a promised create table 11 | let promise = PG.knex.schema.createTable(tablename, (table) => { 12 | table.increments(); 13 | table.string('name'); 14 | }); 15 | return Promise.await(promise); 16 | }; 17 | 18 | dropTable(); // Drop the table (if it exists) 19 | createTable(); // Create the table 20 | Table = new PG.Table('testing'); // Ready to test pg package functionality 21 | }); 22 | 23 | 24 | Tinytest.add('pg - query builder - add rows to testing table', (test) => { 25 | let row; 26 | Table.delete().run(); // Truncate table (only works on the server) 27 | 28 | Table.insert({id:1, name: 'Bob'}).run(); // Add 'Bob' as id 1 29 | row = Table.where({id: 1}).fetch(); // Return row with id 1 30 | test.equal(row.length, 1); // There should be one (and only one) 31 | test.equal(row[0].name, 'Bob'); // And his name should be Bob 32 | 33 | Table.insert({id:2, name: 'Carol'}).run(); // Add 'Carol' as id 2 34 | row = Table.where({id: 2}).fetch(); // Return row with id 2 35 | test.equal(row.length, 1); // There should be one (and only one) 36 | test.equal(row[0].name, 'Carol'); // And her name should be Carol 37 | }); 38 | 39 | Tinytest.add('pg - query builder - check correct row count', (test) => { 40 | const n = +Table.count('* AS n').fetch()[0].n; 41 | test.equal(n, 2); // We inserted two rows, so we should have two rows 42 | }); 43 | 44 | Tinytest.add('pg - query builder - check update works', (test) => { 45 | Table.update({name: 'Ted'}).where({id: 1}).run(); // Change Bob to Ted 46 | const bob = Table.where({id: 1}).fetch()[0]; 47 | test.equal(bob.name, 'Ted'); // Bob should now be Ted 48 | 49 | Table.update({name: 'Alice'}).where({id: 2}).run(); // Change Carol to Alice 50 | const carol = Table.where({id: 2}).fetch()[0]; 51 | test.equal(carol.name, 'Alice'); // Carol should now be Alice 52 | }); 53 | 54 | Tinytest.add('pg - query builder - check fetchOne works', (test) => { 55 | const row = Table.fetchOne(); // Grab a row 56 | test.isTrue(row instanceof Object); // We should have an object 57 | test.isTrue('id' in row); // with an id column 58 | test.isTrue('name' in row); // and a name column 59 | test.isUndefined(Table.where({id: 999}).fetchOne()); // Look for a non-existant id and make sure we get undefined 60 | }); 61 | 62 | Tinytest.add('pg - query builder - check fetchValue works', (test) => { 63 | const n = +Table.count('*').fetchValue(); // Start with a quick count(*) 64 | test.equal(n, 2); // We should have a count of 2 65 | const name = Table.where({id: 2}).fetchValue('name'); //Get Alice 66 | test.equal(name, 'Alice'); // Should be "Alice" 67 | // Look for a non-existant column and make sure we get undefined 68 | test.isUndefined(Table.where({id: 2}).fetchValue('age')); 69 | test.throws(() => { // Make sure "must be a string" is thrown 70 | Table.where({id: 2}).fetchValue({}); 71 | }, 'PG: fetchValue parameter not string'); 72 | // Check findOne's early return for no rows: make sure we get undefined 73 | test.isUndefined(Table.where({id: 999}).fetchValue('name')); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/pg/pg-tests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/postgres-packages/694a96f406b2387b146cb36f353fa00e5b0c6342/packages/pg/pg-tests.js -------------------------------------------------------------------------------- /packages/pg/pg.js: -------------------------------------------------------------------------------- 1 | PG.defaultConnectionUrl = process.env.POSTGRESQL_URL || 'postgres://127.0.0.1/postgres'; 2 | 3 | var pg = Npm.require('pg'); 4 | 5 | PG._npmModule = pg; 6 | 7 | var livePg = new PgLiveQuery({ 8 | connectionUrl: PG.defaultConnectionUrl, 9 | channel: 'simple_pg_' + Random.id(4) 10 | }); 11 | PG._livePg = livePg; 12 | 13 | PG.Query = function (sqlString, params, name) { 14 | if (! name && (typeof params === 'string')) { 15 | name = params; 16 | params = []; 17 | } 18 | 19 | this.name = name; 20 | this.sqlString = sqlString; 21 | this.params = params || []; 22 | }; 23 | 24 | PG.Query.prototype.observe = function (cbs) { 25 | cbs = cbs || {}; 26 | // poll validators shouldn't be empty 27 | var handle = livePg.select(this.sqlString, this.params, {}, cbs); 28 | 29 | return handle; 30 | }; 31 | 32 | PG.Query.prototype._publishCursor = function (sub) { 33 | var table = this.name; 34 | 35 | var self = this; 36 | 37 | var observeHandle = this.observe({ 38 | added: function (doc) { 39 | sub.added(table, String(doc.id), doc); 40 | }, 41 | changed: function (newDoc, oldDoc) { 42 | sub.changed(table, String(newDoc.id), makeChangedFields(newDoc, oldDoc)); 43 | }, 44 | removed: function (doc) { 45 | sub.removed(table, String(doc.id)); 46 | } 47 | }); 48 | 49 | sub.onStop(function () {observeHandle.stop();}); 50 | }; 51 | 52 | 53 | // XXX copy pasted, should be taken from the diff-sequence package once this change is out in Meteor 1.2 54 | var diffObjects = function (left, right, callbacks) { 55 | _.each(left, function (leftValue, key) { 56 | if (_.has(right, key)) 57 | callbacks.both && callbacks.both(key, leftValue, right[key]); 58 | else 59 | callbacks.leftOnly && callbacks.leftOnly(key, leftValue); 60 | }); 61 | if (callbacks.rightOnly) { 62 | _.each(right, function(rightValue, key) { 63 | if (!_.has(left, key)) 64 | callbacks.rightOnly(key, rightValue); 65 | }); 66 | } 67 | }; 68 | 69 | 70 | // XXX copy pasted, should be taken from the diff-sequence package once this change is out in Meteor 1.2 71 | var makeChangedFields = function (newDoc, oldDoc) { 72 | var fields = {}; 73 | diffObjects(oldDoc, newDoc, { 74 | leftOnly: function (key, value) { 75 | fields[key] = undefined; 76 | }, 77 | rightOnly: function (key, value) { 78 | fields[key] = value; 79 | }, 80 | both: function (key, leftValue, rightValue) { 81 | if (!EJSON.equals(leftValue, rightValue)) 82 | fields[key] = rightValue; 83 | } 84 | }); 85 | return fields; 86 | }; 87 | -------------------------------------------------------------------------------- /packages/pg/pre.js: -------------------------------------------------------------------------------- 1 | PG = {}; 2 | -------------------------------------------------------------------------------- /packages/pg/transaction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrap a function in a transaction; rollback if error is thrown when it is called 3 | * @param {Function} func function to run 4 | * @return {Object} wrapped function 5 | */ 6 | PG.wrapWithTransaction = function wrapWithTransaction(func) { 7 | return function wrappedWithTransaction (/*args*/) { 8 | let ret; 9 | 10 | const transactionRun = new Promise((resolve, reject) => { 11 | PG.knex.transaction(Meteor.bindEnvironment((trx) => { 12 | try { 13 | ret = func.apply(this, arguments); 14 | trx.commit(); 15 | resolve(ret); 16 | } catch (e) { 17 | trx.rollback(); 18 | console.log("Error in promise:", e); 19 | reject(e); 20 | } 21 | })); 22 | }); 23 | 24 | return PG.await(transactionRun); 25 | }; 26 | }; 27 | 28 | PG.inTransaction = function inTransaction(func) { 29 | return PG.wrapWithTransaction(func)(); 30 | }; 31 | -------------------------------------------------------------------------------- /run-knex-client-tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | export PACKAGE_DIRS="$(dirname $0)/packages/" 3 | 4 | meteor --release METEOR@1.2-rc.3 test-packages \ 5 | "$PACKAGE_DIRS/bookshelf" 6 | -------------------------------------------------------------------------------- /run-observe-tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | echo "Make sure you have created a DB called 'test'!" 3 | export POSTGRESQL_URL="postgres://127.0.0.1/test" 4 | export PACKAGE_DIRS="$(dirname $0)/packages/" 5 | 6 | echo $PACKAGE_DIRS 7 | 8 | meteor --release METEOR@1.2-rc.3 test-packages \ 9 | "$PACKAGE_DIRS/pg" 10 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | echo "Make sure you have created a DB called 'test'!" 3 | export POSTGRESQL_URL="postgres://127.0.0.1/test" 4 | export PACKAGE_DIRS="$(dirname $0)/packages/" 5 | 6 | echo $PACKAGE_DIRS 7 | 8 | meteor --release METEOR@1.2-rc.3 test-packages \ 9 | "$PACKAGE_DIRS/accounts-base" \ 10 | "$PACKAGE_DIRS/accounts-password" 11 | --------------------------------------------------------------------------------