Skip to content

Commit

Permalink
Merge pull request #247 from dadi/feat/media-endpoint
Browse files Browse the repository at this point in the history
Change media endpoint structure
  • Loading branch information
jimlambie authored May 29, 2017
2 parents c15f9f2 + e96dc08 commit 450540d
Show file tree
Hide file tree
Showing 53 changed files with 2,473 additions and 805 deletions.
150 changes: 150 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,156 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.0.0] (2017-05-29)

### Changed

#### Upgraded MongoDB driver
Upgrade MongoDB driver to 2.2.x, from the existing 1.4.x version.

#### Fixed `create-client` script
Use correct `accessType` property in client store documents

#### Media collections
This version introduces a few changes to how media is handled by API.

The concept of media collections has been abstracted from the public API. It removes the requirement for a collection schema, instead using a schema kept internally in API. At the moment it's hardcoded to store images (containing dimensions, size, mime type, etc.), but in the future we will look into making the schema adapt to the type of file being uploaded.

##### Endpoints

| Method | Endpoint | Purpose | Example
|:-|:---|:----|:--
| POST |`/media/sign`| Requesting a signed URL for a media upload| |
| POST |`/media/:signedUrl`|Uploading a media asset ||
| GET | `/media`|Listing media assets ||
| GET | `/media/:assetPath`|Access a specific media asset | `/media/2017/04/27/flowers.jpg`

#### Media buckets

Even though that's abstracted from the end user, assets still need to be stored in collections. Assets POSTed to /media will be stored in a `mediaStore` collection (configurable via the `media.defaultBucket` configuration parameter). It is also possible to add additional "media buckets", configured as an array in the `media.buckets` configuration parameter.

##### Endpoints

Here are the same media collection endpoints for interacting with a media bucket called `mediaAvatars`:

| Method | Endpoint | Purpose | Example
|:-|:---|:----|:--
| POST |`/media/mediaAvatars/sign`| Requesting a signed URL for a media upload| |
| POST |`/media/mediaAvatars/:signedUrl`|Uploading a media asset ||
| GET | `/media/mediaAvatars`|Listing media assets ||
| GET | `/media/mediaAvatars/:assetPath`|Access a specific media asset | `/media/mediaAvatars/2017/04/27/flowers.jpg`

#### Naming conflicts

If there is a data collection with the same name as one of the media buckets, API throws an error detailing the name of the conflicting collection.

#### Discovering media buckets

Added information about media buckets to the /api/collections endpoint, indicating a list of the available media buckets as well as the name of the default one.

```
GET /api/collections
```

```json
{
"collections": [
{
"version": "1.0",
"database": "library",
"name": "Articles",
"slug": "articles",
"path": "/1.0/library/articles"
},
{
"version": "1.0",
"database": "library",
"name": "Books",
"slug": "books",
"path": "/1.0/library/books"
}
],
"media": {
"buckets": [
"authorImages",
"mediaStore"
],
"defaultBucket": "mediaStore"
}
}
```

#### Add `url` property to media documents
Instead of replacing the contents of `path`, leave that as it is and write the full URL to a new property called `url`.

```json
"image": {
"_id": "591b5f29795b683664af01e9",
"fileName": "3RdYMTLoL1X16djGF52cFtJovDT.jpg",
"mimetype": "image/jpeg",
"width": 600,
"height": 900,
"contentLength": 54907,
"path": "/media/2017/05/16/3RdYMTLoL1X16djGF52cFtJovDT-1494966057926.jpg",
"createdAt": 1494966057685,
"createdBy": null,
"v": 1,
"url": "http://localhost:5000/media/2017/05/16/3RdYMTLoL1X16djGF52cFtJovDT-1494966057926.jpg"
}
```

#### Hook configuration endpoints

Extended the hooks config endpoint (`/api/hooks/:hookName/config`) to accept POST, PUT and DELETE requests to create, update and delete hooks, respectively.

#### Other

* [#245](https://github.com/dadi/api/issues/245): fix media path formatting
* [#260](https://github.com/dadi/api/issues/260): modify property value that indicates a media collection to "mediaCollection"
* [#265](https://github.com/dadi/api/issues/265): array not validated against schemas in POST requests
* [#284](https://github.com/dadi/api/issues/284): indexes not checked correctly when given a sort key
* remove `apiVersion` query property when composing reference fields, improves performance

### Added

#### MongoDB readPreference configuration
Added `readPreference` configuration option. Default is `secondaryPreferred`. Closed [#156](https://github.com/dadi/api/issues/156)

```json
"database": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
],
"username": "",
"password": "",
"database": "api",
"ssl": false,
"replicaSet": "",
"enableCollectionDatabases": false,
"readPreference": "primary"
}
```

#### API baseUrl

We've introduced a `server.baseUrl` configuration parameter, which will be used to determine the URL of media assets when using the disk storage option.

```json
"baseUrl": {
"protocol": "http",
"port": 80,
"host": "mydomain.com"
}
```


#### Post install script

Added a post install script which runs following an install of API from NPM. A development configuration file is created along with a basic workspace directory containing two collections, an endpoint and a hook. No files are overwritten if the config and workspace directories already exist.

## [1.16.6] (2017-05-25)

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<img src="https://dadi.tech/assets/products/dadi-api-full.png" alt="DADI API" height="65"/>

[![npm (scoped)](https://img.shields.io/npm/v/@dadi/api.svg?maxAge=10800&style=flat-square)](https://www.npmjs.com/package/@dadi/api)
[![coverage](https://img.shields.io/badge/coverage-87%25-yellow.svg?style=flat?style=flat-square)](https://github.com/dadi/api)
[![coverage](https://img.shields.io/badge/coverage-88%25-yellow.svg?style=flat?style=flat-square)](https://github.com/dadi/api)
[![Build Status](https://travis-ci.org/dadi/api.svg?branch=master)](https://travis-ci.org/dadi/api)
[![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com/)

Expand Down
35 changes: 35 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ var conf = convict({
default: 'DADI API Repo Default',
}
},
publicUrl: {
host: {
doc: 'The host of the URL where the API instance can be publicly accessed at',
format: '*',
default: null,
env: 'URL_HOST'
},
port: {
doc: 'The port of the URL where the API instance can be publicly accessed at',
format: '*',
default: null,
env: 'URL_PORT'
},
protocol: {
doc: 'The protocol of the URL where the API instance can be publicly accessed at',
format: 'String',
default: 'http',
env: 'URL_PROTOCOL'
}
},
server: {
host: {
doc: 'Accept connections on the specified address. If the host is omitted, the server will accept connections on any IPv6 address (::) when IPv6 is available, or any IPv4 address (0.0.0.0) otherwise.',
Expand Down Expand Up @@ -109,6 +129,11 @@ var conf = convict({
doc: '',
format: String,
default: ''
},
readPreference: {
doc: "Choose how MongoDB routes read operations to the members of a replica set - see https://docs.mongodb.com/manual/reference/read-preference/",
format: ['primary', 'primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'],
default: 'secondaryPreferred'
},
enableCollectionDatabases: {
doc: '',
Expand Down Expand Up @@ -292,6 +317,16 @@ var conf = convict({
}
},
media: {
defaultBucket: {
doc: 'The name of the default media bucket',
format: String,
default: 'mediaStore'
},
buckets: {
doc: 'The names of media buckets to be used',
format: Array,
default: []
},
tokenSecret: {
doc: 'The secret key used to sign and verify tokens when uploading media',
format: String,
Expand Down
7 changes: 6 additions & 1 deletion config/config.development.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"host": "127.0.0.1",
"port": 3000
},
"publicUrl": {
"host": "localhost",
"port": 3000
},
"database": {
"hosts": [
{
Expand Down Expand Up @@ -74,7 +78,8 @@
},
"paths": {
"collections": "workspace/collections",
"endpoints": "workspace/endpoints"
"endpoints": "workspace/endpoints",
"hooks": "workspace/hooks"
},
"logging": {
"enabled": true,
Expand Down
7 changes: 6 additions & 1 deletion config/config.production.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"host": "127.0.0.1",
"port": 3000
},
"publicUrl": {
"host": "localhost",
"port": 3000
},
"database": {
"hosts": [
{
Expand Down Expand Up @@ -74,7 +78,8 @@
},
"paths": {
"collections": "workspace/collections",
"endpoints": "workspace/endpoints"
"endpoints": "workspace/endpoints",
"hooks": "workspace/hooks"
},
"logging": {
"enabled": true,
Expand Down
7 changes: 6 additions & 1 deletion config/config.qa.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"host": "127.0.0.1",
"port": 3000
},
"publicUrl": {
"host": "localhost",
"port": 3000
},
"database": {
"hosts": [
{
Expand Down Expand Up @@ -74,7 +78,8 @@
},
"paths": {
"collections": "workspace/collections",
"endpoints": "workspace/endpoints"
"endpoints": "workspace/endpoints",
"hooks": "workspace/hooks"
},
"logging": {
"enabled": true,
Expand Down
11 changes: 10 additions & 1 deletion config/config.test.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"host": "127.0.0.1",
"port": 8000
},
"publicUrl": {
"host": "api.somedomain.tech",
"port": 80
},
"database": {
"hosts": [
{
Expand Down Expand Up @@ -74,7 +78,8 @@
},
"paths": {
"collections": "test/acceptance/workspace/collections",
"endpoints": "test/acceptance/workspace/endpoints"
"endpoints": "test/acceptance/workspace/endpoints",
"hooks": "test/acceptance/workspace/hooks"
},
"logging": {
"enabled": true,
Expand All @@ -83,6 +88,10 @@
"filename": "dadi-api",
"extension": "log"
},
"media": {
"defaultBucket": "mediaStore",
"basePath": "test/acceptance/workspace/media"
},
"feedback": false,
"cors": false
}
37 changes: 34 additions & 3 deletions dadi/lib/api/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var _ = require('underscore')
var fs = require('fs')
var http = require('http')
var https = require('https')
Expand Down Expand Up @@ -88,9 +89,12 @@ Api.prototype.use = function (path, handler) {
return this.all.push(path)
}

var regex = pathToRegexp(path)

this.paths[path] = {
handler: handler,
regex: pathToRegexp(path)
order: routePriority(path, regex.keys),
regex: regex
}
}

Expand Down Expand Up @@ -236,15 +240,15 @@ Api.prototype._match = function (path, req) {
// always add params object to avoid need for checking later
req.params = {}

Object.keys(paths).forEach(function (key) {
Object.keys(paths).forEach((key) => {
var match = paths[key].regex.exec(path)
if (!match) return

var keys = paths[key].regex.keys

handlers.push(paths[key].handler)

match.forEach(function (k, i) {
match.forEach((k, i) => {
var keyOpts = keys[i] || {}
if (match[i + 1] && keyOpts.name) req.params[keyOpts.name] = match[i + 1]
})
Expand Down Expand Up @@ -286,3 +290,30 @@ function notFound (req, res) {
res.end()
}
}

function routePriority (path, keys) {
var tokens = pathToRegexp.parse(path)

var staticRouteLength = 0
if (typeof tokens[0] === 'string') {
staticRouteLength = _.compact(tokens[0].split('/')).length
}

var requiredParamLength = _.filter(keys, function (key) {
return !key.optional
}).length

var optionalParamLength = _.filter(keys, function (key) {
return key.optional
}).length

var order =
staticRouteLength * 5 +
requiredParamLength * 2 +
optionalParamLength

// make internal routes less important...
if (path.indexOf('/api/') > 0) order = -100

return order
}
Loading

0 comments on commit 450540d

Please sign in to comment.