Skip to content
This repository has been archived by the owner on Apr 4, 2022. It is now read-only.

Commit

Permalink
Fix #104: Add support for the attachment API. (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 authored Jul 18, 2016
1 parent 302f6d9 commit 48eb04e
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 79 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ before_install:
- virtualenv .env
- if [[ $SERVER && $SERVER != development ]]; then .env/bin/pip install kinto==$SERVER; fi
- if [[ $SERVER && $SERVER == development ]]; then .env/bin/pip install https://github.com/Kinto/kinto/zipball/master; fi
- .env/bin/pip install kinto-attachment
- export KINTO_PSERVE_EXECUTABLE=.env/bin/pserve
script:
- npm $ACTION
Expand Down
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ Read the [API documentation](https://doc.esdoc.org/github.com/Kinto/kinto-http.j
- [Deleting record](#deleting-record)
- [Listing records](#listing-records)
- [Batching operations](#batching-operations)
- [Options](#options)
- [Attachments](#attachments)
- [Adding an attachment to a record](#adding-an-attachment-to-a-record)
- [Updating an attachment](#updating-an-attachment)
- [Deleting an attachment](#deleting-an-attachment)
- [Generic bucket and collection options](#generic-bucket-and-collection-options)
- [The safe option explained](#the-safe-option-explained)
- [Safe creations](#safe-creations)
- [Safe updates](#safe-updates)
Expand Down Expand Up @@ -1191,7 +1195,41 @@ Sample result:
}
```
## Options
## Attachments
If the [attachment](https://github.com/Kinto/kinto-attachment) capability is available from the Kinto server, you can attach files to records. Files must be passed as [data urls](http://dataurl.net/#about), which can be generated using the [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) in the browser.
### Adding an attachment to a record
```js
client.bucket("blog").collection("posts")
.addAttachment(dataURL, {title: "First post"});
```
#### Options
- `headers`: Custom headers object to send along the HTTP request;
- `safe`: Ensures operations won't override existing resources on the server if their associated `last_modified` value or option are provided; otherwise ensures resources won't be overriden if they already exist on the server;
- `last_modified`: When `safe` is true, the last timestamp we know the resource has been updated on the server;
- `permissions`: Permissions to be set on the record;
- `filename`: Allows to specify the attachment filename, in case the data URI does not contain any, or if the file has to be renamed on upload;
### Updating an attachment
```js
client.bucket("blog").collection("posts")
.addAttachment(dataURL, {id: "22c1319e-7b09-46db-bec4-c240bdf4e3e9"});
```
### Deleting an attachment
```js
client.bucket("blog").collection("posts")
.removeAttachment("22c1319e-7b09-46db-bec4-c240bdf4e3e9");
```
## Generic bucket and collection options
Both `bucket()` and `collection()` methods accept an `options` object as a second arguments where you can define the following options:
Expand Down
20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
"main": "lib/index.js",
"scripts": {
"build": "babel -d lib/ src/",
"build:readme": "./node_modules/.bin/toctoc -w -d 2 README.md",
"dist": "mkdir -p dist && rm -f dist/*.* && npm run dist-dev && npm run dist-prod && npm run dist-noshim && npm run dist-fx",
"dist-dev": "browserify -s KintoClient -d -e src/index.js -o dist/kinto-http.js -t [ babelify --sourceMapRelative . ]",
"dist-noshim": "browserify -s KintoClient -g uglifyify --ignore isomorphic-fetch --ignore babel-polyfill -e src/index.js -o dist/kinto-http.noshim.js -t [ babelify --sourceMapRelative . ]",
"dist-prod": "browserify -s KintoClient -g uglifyify -e src/index.js -o dist/kinto-http.min.js -t [ babelify --sourceMapRelative . ]",
"dist-fx": "BABEL_ENV=firefox browserify -s KintoHttpClient --ignore isomorphic-fetch --ignore events --bare -e fx-src/index.js -o temp.jsm -t [ babelify --sourceMapRelative . ] && mkdir -p dist && cp fx-src/jsm_prefix.js dist/moz-kinto-http-client.js && echo \"\n/*\n * Version $npm_package_version - $(git rev-parse --short HEAD)\n */\n\" >> dist/moz-kinto-http-client.js && cat temp.jsm >> dist/moz-kinto-http-client.js && rm temp.jsm",
"prepublish": "node_modules/.bin/toctoc -w -d 2 README.md",
"prepublish": "npm run build:readme",
"publish-to-npm": "npm run build && npm run dist && npm publish",
"report-coverage": "npm run test-cover && ./node_modules/coveralls/bin/coveralls.js < ./coverage/lcov.info",
"tdd": "babel-node node_modules/.bin/_mocha --watch 'test/**/*_test.js'",
"tdd": "babel-node node_modules/.bin/_mocha --require ./test/setup-jsdom.js --watch 'test/**/*_test.js'",
"test": "npm run test-nocover",
"test-cover": "babel-node node_modules/.bin/babel-istanbul cover --report text $npm_package_config_ISTANBUL_OPTS node_modules/.bin/_mocha -- 'test/**/*_test.js'",
"test-cover-html": "babel-node node_modules/.bin/babel-istanbul cover --report html $npm_package_config_ISTANBUL_OPTS node_modules/.bin/_mocha -- 'test/**/*_test.js' && open coverage/index.html",
"test-nocover": "babel-node node_modules/.bin/_mocha 'test/**/*_test.js'",
"test-cover": "babel-node node_modules/.bin/babel-istanbul cover --report text $npm_package_config_ISTANBUL_OPTS node_modules/.bin/_mocha -- --require ./test/setup-jsdom.js 'test/**/*_test.js'",
"test-cover-html": "babel-node node_modules/.bin/babel-istanbul cover --report html $npm_package_config_ISTANBUL_OPTS node_modules/.bin/_mocha -- --require ./test/setup-jsdom.js 'test/**/*_test.js' && open coverage/index.html",
"test-nocover": "babel-node node_modules/.bin/_mocha --require ./test/setup-jsdom.js 'test/**/*_test.js'",
"lint": "eslint src test"
},
"repository": {
Expand All @@ -37,9 +38,11 @@
},
"homepage": "https://github.com/Kinto/kinto-http.js#readme",
"dependencies": {
"isomorphic-fetch": "^2.2.1"
"isomorphic-fetch": "^2.2.1",
"uuid": "^2.0.1"
},
"devDependencies": {
"atob": "^2.0.3",
"babel-cli": "^6.6.5",
"babel-core": "^6.6.5",
"babel-eslint": "^5.0.0-beta6",
Expand All @@ -60,12 +63,13 @@
"esdoc-es7-plugin": "0.0.3",
"esdoc-importpath-plugin": "0.0.1",
"eslint": "2.2.0",
"form-data": "^1.0.0-rc4",
"jsdom": "^9.4.1",
"kinto-node-test-server": "0.0.1",
"mocha": "^2.3.4",
"sinon": "^1.17.2",
"toctoc": "^0.2.2",
"uglifyify": "^3.0.1",
"uuid": "^2.0.1"
"uglifyify": "^3.0.1"
},
"engines": {
"node": ">=6"
Expand Down
13 changes: 8 additions & 5 deletions src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,27 +343,30 @@ export default class KintoClientBase {
* @private
* @param {Object} request The request object.
* @param {Object} [options={}] The options object.
* @param {Boolean} [options.raw=false] If true, resolve with full response object, including json body and headers instead of just json.
* @param {Boolean} [options.raw=false] If true, resolve with full response
* @param {Boolean} [options.stringify=true] If true, serialize body data to
* JSON.
* @return {Promise<Object, Error>}
*/
execute(request, options={raw: false}) {
execute(request, options={raw: false, stringify: true}) {
const {raw, stringify} = options;
// If we're within a batch, add the request to the stack to send at once.
if (this._isBatch) {
this._requests.push(request);
// Resolve with a message in case people attempt at consuming the result
// from within a batch operation.
const msg = "This result is generated from within a batch " +
"operation and should not be consumed.";
return Promise.resolve(options.raw ? {json: msg} : msg);
return Promise.resolve(raw ? {json: msg} : msg);
}
const promise = this.fetchServerSettings()
.then(_ => {
return this.http.request(this.remote + request.path, {
...request,
body: JSON.stringify(request.body)
body: stringify ? JSON.stringify(request.body) : request.body,
});
});
return options.raw ? promise : promise.then(({json}) => json);
return raw ? promise : promise.then(({json}) => json);
}

/**
Expand Down
55 changes: 50 additions & 5 deletions src/collection.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { toDataBody, qsify, isObject } from "./utils";
import { v4 as uuid } from "uuid";

import { capable, toDataBody, qsify, isObject } from "./utils";
import * as requests from "./requests";
import endpoint from "./endpoint";

Expand Down Expand Up @@ -150,10 +152,11 @@ export default class Collection {
/**
* Creates a record in current collection.
*
* @param {Object} record The record to create.
* @param {Object} [options={}] The options object.
* @param {Object} [options.headers] The headers object option.
* @param {Boolean} [options.safe] The safe option.
* @param {Object} record The record to create.
* @param {Object} [options={}] The options object.
* @param {Object} [options.headers] The headers object option.
* @param {Boolean} [options.safe] The safe option.
* @param {Object} [options.permissions] The permissions option.
* @return {Promise<Object, Error>}
*/
createRecord(record, options={}) {
Expand All @@ -164,6 +167,47 @@ export default class Collection {
return this.client.execute(request);
}

/**
* Adds an attachment to a record, creating the record when it doesn't exist.
*
* @param {String} dataURL The data url.
* @param {Object} [record={}] The record data.
* @param {Object} [options={}] The options object.
* @param {Object} [options.headers] The headers object option.
* @param {Boolean} [options.safe] The safe option.
* @param {Number} [options.last_modified] The last_modified option.
* @param {Object} [options.permissions] The permissions option.
* @param {String} [options.filename] Force the attachment filename.
* @return {Promise<Object, Error>}
*/
@capable(["attachments"])
addAttachment(dataURI, record={}, options={}) {
const reqOptions = this._collOptions(options);
const {permissions} = reqOptions;
const id = record.id || uuid.v4();
const path = endpoint("attachment", this.bucket.name, this.name, id);
const addAttachmentRequest = requests.addAttachmentRequest(path, dataURI, {data: record, permissions}, reqOptions);
return this.client.execute(addAttachmentRequest, {stringify: false})
.then(() => this.getRecord(id));
}

/**
* Removes an attachment from a given record.
*
* @param {Object} recordId The record id.
* @param {Object} [options={}] The options object.
* @param {Object} [options.headers] The headers object option.
* @param {Boolean} [options.safe] The safe option.
* @param {Number} [options.last_modified] The last_modified option.
*/
@capable(["attachments"])
removeAttachment(recordId, options={}) {
const reqOptions = this._collOptions(options);
const path = endpoint("attachment", this.bucket.name, this.name, recordId);
const request = requests.deleteRequest(path, reqOptions);
return this.client.execute(request);
}

/**
* Updates a record in current collection.
*
Expand All @@ -172,6 +216,7 @@ export default class Collection {
* @param {Object} [options.headers] The headers object option.
* @param {Boolean} [options.safe] The safe option.
* @param {Number} [options.last_modified] The last_modified option.
* @param {Object} [options.permissions] The permissions option.
* @return {Promise<Object, Error>}
*/
updateRecord(record, options={}) {
Expand Down
20 changes: 14 additions & 6 deletions src/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
* @type {Object}
*/
const ENDPOINTS = {
root: () => "/",
batch: () => "/batch",
bucket: (bucket) => "/buckets" + (bucket ? `/${bucket}` : ""),
collection: (bucket, coll) => `${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""),
group: (bucket, group) => `${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""),
record: (bucket, coll, id) => `${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""),
root: () =>
"/",
batch: () =>
"/batch",
bucket: (bucket) =>
"/buckets" + (bucket ? `/${bucket}` : ""),
collection: (bucket, coll) =>
`${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""),
group: (bucket, group) =>
`${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""),
record: (bucket, coll, id) =>
`${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""),
attachment: (bucket, coll, id) =>
`${ENDPOINTS.record(bucket, coll, id)}/attachment`,
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default class HTTP {
request(url, options={headers:{}}) {
let response, status, statusText, headers, hasTimedout;
// Ensure default request headers are always set
options.headers = Object.assign({}, HTTP.DEFAULT_REQUEST_HEADERS, options.headers);
options.headers = {...HTTP.DEFAULT_REQUEST_HEADERS, ...options.headers};
options.mode = this.requestMode;
return new Promise((resolve, reject) => {
const _timeoutId = setTimeout(() => {
Expand Down
31 changes: 29 additions & 2 deletions src/requests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { omit } from "./utils";
import { omit, createFormData } from "./utils";


const requestDefaults = {
safe: false,
Expand Down Expand Up @@ -74,7 +75,7 @@ export function updateRequest(path, {data, permissions}, options={}) {
* @private
*/
export function deleteRequest(path, options={}) {
const { headers, safe, last_modified} = {
const {headers, safe, last_modified} = {
...requestDefaults,
...options
};
Expand All @@ -87,3 +88,29 @@ export function deleteRequest(path, options={}) {
headers: {...headers, ...safeHeader(safe, last_modified)}
};
}

/**
* @private
*/
export function addAttachmentRequest(path, dataURI, {data, permissions}={}, options={}) {
const {headers, safe} = {...requestDefaults, ...options};
const {last_modified} = {...data, ...options };
if (safe && !last_modified) {
throw new Error("Safe concurrency check requires a last_modified value.");
}

const body = {data, permissions};
const formData = createFormData(dataURI, body, options);

return {
method: "POST",
path,
headers: {
...headers,
...safeHeader(safe, last_modified),
// Setting content type as undefined so that it does not use default "application/json".
"Content-Type": undefined
},
body: formData
};
}
Loading

0 comments on commit 48eb04e

Please sign in to comment.