kinto-clientdeprecated

JavaScript HTTP client for the Kinto API.

Usage no npm install needed!

<script type="module">
  import kintoClient from 'https://cdn.skypack.dev/kinto-client';
</script>

README

kinto-client

Build Status

A JavaScript HTTP Client for the Kinto API.

Read the API documentation.

Table of Contents


Installation

In the browser, you can load prebuilt scripts hosted on npmcdn:

<script src="https://npmcdn.com/kinto-client/dist/kinto-client.min.js"></script>

In nodejs:

$ npm install kinto-client --save

Then (ES6):

import KintoClient from "kinto-client";

Or (ES5):

var KintoClient = require("kinto-client").default;

Note that this HTTP client can be transparently used server side or in a regular browser page.

Usage

A client instance is created using the KintoClient constructor, passing it the remote Kinto server root URL, including the version:

const client = new KintoClient("https://kinto.dev.mozaws.net/v1");

Options

  • safe: Adds concurrency headers to every requests. (default: true)
  • events: The events handler. If none provided an EventEmitter instance will be created.
  • headers: The key-value headers to pass to each request. (default: {})
  • bucket: The default bucket to use. (default: "default")
  • requestMode: The HTTP CORS mode. (default: "cors")
  • timeout: The requests timeout in milliseconds. (default: 5000)

Authentication

Authenticating against a Kinto server can be achieved by adding an Authorization header to the request.

By default Kinto server supports Basic Auth authentication, but others mechanisms can be activated such as OAuth (eg. Firefox Account)

Using Basic Auth

Simply provide an Authorization header option to the Kinto constructor:

const secretString = `${username}:${password}`;
const kinto = new KintoClient("https://my.server.tld/v1", {
  headers: {
    Authorization: "Basic " + btoa(secretString)
  }
});

Notes

  • As explained in the server docs, any string is accepted. You're not obliged to use the username:password format.

Using an OAuth Bearer Token

As for Basic Auth, once you have retrieved a valid OAuth Bearer Token, simply pass it in an Authorization header:

const kinto = new KintoClient("https://my.server.tld/v1", {
  headers: {
    Authorization: `Bearer ` + oauthBearerToken)
  }
});

Server information

A Kinto server exposes some of its internal settings, information about authenticated user, the HTTP API version and the API capabilities (e.g. plugins).

client.fetchServerInfo([options])
  .then(({data}) => ...);

Sample result:

{
    "project_name": "kinto",
    "project_version": "3.0.2",
    "url": "http://0.0.0.0:8889/v1/",
    "project_docs": "https://kinto.readthedocs.io/",
    "http_api_version": "1.6",
    "settings": {
        "batch_max_requests": 25,
        "readonly": false
    },
    "user": {
        "bucket": "2f9b1aaa-552d-48e8-1b78-371dd08688b3",
        "id": "basicauth:f505765817a6b4ea46278be0620ddedd83b10f71f7695683719fe001cf0871d7"
    },
    "capabilities": {
        "default_bucket": {
            "description": "The default bucket is an alias for a personal bucket where collections are created implicitly.",
            "url": "http://kinto.readthedocs.io/en/latest/api/1.x/buckets.html#personal-bucket-default"
        }
    }
}

Options

  • headers: Custom headers object to send along the HTTP request

Helpers

  • fetchServerSettings([options]): server settings
  • fetchServerCapabilities([options]): API capabilities
  • fetchUser(): authenticated user information
  • fetchHTTPApiVersion([options]): HTTP API version

Buckets

Listing buckets

client.listBuckets([options])
  .then(({data}) => ...);

Sample result:

{
  data: [
    {
      id: "comments",
      last_modified: 1456182233221,
    },
    {
      id: "blog",
      last_modified: 1456181213214,
    },
  ]
}

Options

  • headers: Custom headers object to send along the HTTP request

Creating a new bucket

client.createBucket("blog"[, options])
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456182233221,
    "id": "foo"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Options

  • data: Arbitrary data to attach to the bucket
  • headers: Custom headers object to send along the HTTP request
  • safe: Whether to override existing resource if it already exists (default: false)

Selecting a bucket

client.bucket("blog");

Getting bucket data

client.bucket("blog").getData()
  .then(result => ...);

Sample result:

{
  "last_modified": 1456182336242,
  "id": "blog",
  "foo": "bar"
}

Options

  • headers: Custom headers object to send along the HTTP request

Setting bucket data

client.bucket("blog").setData({foo: "bar"})
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456182336242,
    "id": "blog",
    "foo": "bar"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Options

  • patch: Patches existing bucket data instead of replacing them (default: false)
  • headers: Custom headers object to send along the HTTP request
  • safe: Whether to override existing resource if it already exists (default: false)

Getting bucket permissions

client.bucket("blog").getPermissions()
  .then(result => ...);

Sample result:

{
  "write": [
    "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
  ]
}

Options

  • headers: Custom headers object to send along the HTTP request

Setting bucket permissions

const permissions = {
  read:  ["github:bob"],
  write: ["github:bob", "github:john"]
};

client.bucket("blog").setPermissions(permissions[, options])
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456182888466,
    "id": "blog"
  },
  "permissions": {
    "read": ["github:bob"],
    "write": [
      "github:bob",
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8",
      "github:john"
    ]
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: If last_modified is provided, ensures the resource hasn't been modified since that timestamp. Otherwise ensures no existing resource with the provided id will be overriden (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Notes

  • This operation replaces any previously set permissions;
  • Owners will always keep their write permission bit, as per the Kinto protocol.

Deleting a bucket

client.deleteBucket("testbucket"[, options])
  .then(result => ...);

Sample result:

{
  "data": {
    "deleted": true,
    "last_modified": 1456182931974,
    "id": "blog"
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Ensures the resource hasn't been modified in the meanwhile if last_modified is provided (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Creating a collection

Named collection

client.bucket("blog").createCollection("posts")
  .then(result => ...);

Sample result:


{
  "data": {
    "last_modified": 1456183004372,
    "id": "posts"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

With an ID generated automatically

client.bucket("blog").createCollection()
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183040592,
    "id": "OUh5VEDa"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Note that OUh5VEDa is the collection ID automatically generated by the server.

Options

  • headers: Custom headers object to send along the HTTP request
  • safe: Whether to override existing resource if it already exists (default: false)

Note: For generated names, options can be specified only if the first parameters are provided: createCollection(undefined, {safe: true})

Listing bucket collections

client.bucket("blog").listCollections()
  .then(({data}) => ...);

Sample result:

{
  data: [
    {
      "last_modified": 1456183153840,
      "id": "posts"
    },
    {
      "last_modified": 1456183159386,
      "id": "comments"
    }
  ]
}

Options

  • headers: Custom headers object to send along the HTTP request

Deleting a collection

client.bucket("blog").deleteCollection("test")
  .then(result => ...);

Sample result:

{
  "data": {
    "deleted": true,
    "last_modified": 1456183116571,
    "id": "posts"
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Ensures the resource hasn't been modified in the meanwhile if last_modified is provided (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Creating a user group

Kinto has a concept of groups of users. A group has a list of members and belongs to a bucket.

Permissions can refer to the group instead of an individuals - this makes it easy to define «roles», especially if the same set of permissions is applied to several objects.

When used in permissions definitions, the full group URI has to be used:

    {
      data: {
        title: "My article"
      },
      permissions: {
        write: ["/buckets/blog/groups/authors", "github:lili"],
        read: ["system.Everyone"]
      }
    }

Named group

client.bucket("blog").createGroup("admins")
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183004372,
    "id": "admins",
    "members": []
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

With a list of members and attributes

client.bucket("blog").createGroup("admins", ["system.Authenticated"], {data: {pi: 3.14}})
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183004372,
    "id": "admins",
    "members": ["system.Authenticated"],
    "pi": 3.14
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

With an ID generated automatically

client.bucket("blog").createGroup()
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183040592,
    "members": [],
    "id": "7YHFF565"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Note that 7YHFF565 is the group ID automatically generated by the server.

Options

  • headers: Custom headers object to send along the HTTP request
  • safe: Whether to override existing resource if it already exists (default: false)
  • data: Extra group attributes.
  • permissions: Permissions to be set on the created group.

Note: For generated names, options can be specified only if the first parameters are provided: createGroup(undefined, [], {safe: true})

Listing bucket groups

client.bucket("blog").listGroups()
  .then(({data}) => ...);

Sample result:

{
  "data": [
    {
      "last_modified": 1456183153840,
      "id": "admins",
      "members": ["system.Authenticated"],
      "pi": 3.14
    },
    {
      "last_modified": 1456183159386,
      "id": "moderators",
      "members": ["github:lili"]
    }
  ]
}

Options

  • headers: Custom headers object to send along the HTTP request

Getting a bucket group

client.bucket("blog").getGroup("admins")
  .then(({data}) => ...);

Sample result:

{
  "data": {
      "last_modified": 1456183153840,
      "id": "admins",
      "members": ["system.Authenticated"],
      "pi": 3.14
  }
}

Options

  • headers: Custom headers object to send along the HTTP request

Updating an existing group

const updated = {
  id: "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
  members: ["system.Everyone", "github:lili"],
  pi: 3.141592
};

client.bucket("blog").updateGroup(updated, {permissions: {write: ["fxa:35478"]}})
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183778891,
    "id": "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
    "members": ["system.Everyone", "github:lili"],
    "pi": 3.141592
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8",
      "fxa:35478"
    ]
  }
}

Options

  • patch: Patches the existing record instead of replacing it (default: false)
  • headers: Custom headers object to send along the HTTP request;
  • safe: If last_modified is provided, ensures the resource hasn't been modified since that timestamp. Otherwise ensures no existing resource with the provided id will be overriden (default: false);
  • permissions: Permissions to be set on the group.

Deleting a group

client.bucket("blog").deleteGroup("admins")
  .then(result => ...);

Sample result:

{
  "data": {
    "deleted": true,
    "last_modified": 1456183116571,
    "id": "admins"
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Ensures the resource hasn't been modified in the meanwhile if last_modified is provided (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Collections

Selecting a collection

client.bucket("blog").collection("posts")
  .then(result => ...);

Getting collection data

client.bucket("blog").collection("posts").getData()
  .then(result => ...);

Sample result:

{
  "last_modified": 1456183561206,
  "id": "posts",
  "preferedAuthor": "@chucknorris"
}

Options

  • headers: Custom headers object to send along the HTTP request

Setting collection data

client.bucket("blog").collection("posts")
  .setData({preferedAuthor: "@chucknorris"})
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183561206,
    "id": "posts",
    "preferedAuthor": "@chucknorris"
  },
  "permissions": {
    "write": [
      "github:bob",
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8",
      "github:john"
    ]
  }
}

Options

  • patch: Patches the existing data instead of replacing them (default: false)
  • headers: Custom headers object to send along the HTTP request;
  • safe: If last_modified is provided, ensures the resource hasn't been modified since that timestamp. Otherwise ensures no existing resource with the provided id will be overriden (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Getting collection permissions

client.bucket("blog").collection("posts").getPermissions()
  .then(result => ...);

Sample result:

{
  "write": [
    "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8",
  ]
}

Options

  • headers: Custom headers object to send along the HTTP request

Setting collection permissions

client.bucket("blog").collection("posts")
  .setPermissions({
    read: ["github:bob"],
    write: ["github:john", "github:bob"]
  })
  .then(result => ...);

Sample result:

{
  "data": {
    "last_modified": 1456183508926,
    "id": "posts"
  },
  "permissions": {
    "read": ["github:bob"],
    "write": [
      "github:bob",
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8",
      "github:john"
    ]
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: If last_modified is provided, ensures the resource hasn't been modified since that timestamp. Otherwise ensures no existing resource with the provided id will be overriden (default: false);
  • last_modified: The last timestamp we know the resource has been updated on the server.

Notes

  • This operation replaces any previously set permissions;
  • Owners will always keep their write permission bit, as per the Kinto protocol.

Creating a new record

client.bucket("blog").collection("posts")
  .createRecord({title: "My first post", content: "Hello World!"})
  .then(result => ...);

Sample result:

{
  "data": {
    "content": "Hello World!",
    "last_modified": 1456183657846,
    "id": "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
    "title": "My first post"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Whether to override existing resource if it already exists and if an id is provided (default: false)

Retrieving an existing record

client.bucket("blog").collection("posts")
  .getRecord("cb0f7b2b-e78f-41a8-afad-92a56f8c88db")
  .then(result => ...);

Sample result:

{
  "data": {
    "content": "Hello World!",
    "last_modified": 1456183657846,
    "id": "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
    "title": "My first post"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Ensures an existing record with this ID won't be overridden (default: false).

Updating an existing record

const updated = {
  id: "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
  title: "My first post, edited",
  content: "Hello World, again!"
};

client.bucket("blog").collection("posts")
  .updateRecord(updated)
  .then(result => ...);

Sample result:

{
  "data": {
    "content": "Hello World, again!",
    "last_modified": 1456183778891,
    "id": "cb0f7b2b-e78f-41a8-afad-92a56f8c88db",
    "title": "My first post, edited"
  },
  "permissions": {
    "write": [
      "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
    ]
  }
}

Options

  • patch: Patches the existing record instead of replacing it (default: false)
  • headers: Custom headers object to send along the HTTP request;
  • safe: If last_modified is provided, ensures the resource hasn't been modified since that timestamp. Otherwise ensures no existing resource with the provided id will be overriden (default: false);

Deleting record

client.bucket("blog").collection("posts")
  .deleteRecord("cb0f7b2b-e78f-41a8-afad-92a56f8c88db")
  .then(result => ...);

Sample result:

{
  "data": {
    "deleted": true,
    "last_modified": 1456183877287,
    "id": "cb0f7b2b-e78f-41a8-afad-92a56f8c88db"
  }
}

Options

  • headers: Custom headers object to send along the HTTP request;
  • safe: Ensures the resource hasn't been modified in the meanwhile if last_modified is provided (default: false);
  • last_modified: When safe is true, the last timestamp we know the resource has been updated on the server.

Listing records

client.bucket("blog").collection("posts")
  .listRecords()
  .then(result => ...);

Sample result:

{
  last_modified: "\"1456183930780\"",
  next: <Function>,
  data: [
    {
      "content": "True.",
      "last_modified": 1456183930780,
      "id": "a89dd4b2-d597-4192-bc2b-834116244d29",
      "title": "I love cheese"
    },
    {
      "content": "Yo",
      "last_modified": 1456183914275,
      "id": "63c1805a-565a-46cc-bfb3-007dfad54065",
      "title": "Another post"
    }
  ]
}

Note the root last_modified value which is the collection's timestamp. This value is opaque and should be reused as is, eg. passing it as a since option (see the Options section below).

Sorting

By default, records are listed by last_modified descending order. You can set the sort option to order by another field:

client.bucket("blog").collection("posts")
  .listRecords({sort: "title"})
  .then(({data, next}) => {

Polling for changes

To retrieve the list of records modified since a given timestamp, use the since option:

client.bucket("blog").collection("posts")
  .listRecords({since: "\"1456183930780\""})
  .then(({data, next}) => {

Pagination

By default, all records returned by the server are retrieved. To specify a max number of records to retrieve, you can use the limit option:

client.bucket("blog").collection("posts")
  .listRecords({limit: 20})
  .then(({data, next}) => {

To retrieve the next page of records, just call next() from the result object obtained. If no next page is available, next() throws an error you can catch to exit the flow:

let getNextPage;

client.bucket("blog").collection("posts")
  .listRecords({limit: 20})
  .then(({data, next}) => {
    console.log("Page 1", data);
    getNextPage = next;
  });

Later down the flow:

getNextPage()
  .then(({data, next}) => {
    console.log("Page 2", data);
  })
  .catch(_ => {
    console.log("No more pages.");
  });

Last, if you just want to retrieve and aggregate a given number of result pages, instead of dealing with calling next() recursively you can simply specify the pages option:

client.bucket("blog").collection("posts")
  .listRecords({limit: 20, pages: 3})
  .then(({data, next}) => ...); // A maximum of 60 records will be retrieved here
Notes

If you plan on fetching all the available pages, you can set the pages option to Infinity. Be aware that for large datasets this strategy can possibly issue an important amount of HTTP requests.

Options

  • sort: The order field (default: -last_modified);
  • pages: The number of result pages to retrieve (default: 1);
  • limit: The number of records to retrieve per page: unset by default, uses default server configuration;
  • filters: An object defining the filters to apply; read more about what's supported;
  • since: The ETag header value received from the last response from the server.
  • headers: Custom headers object to send along the HTTP request;

Batching operations

This allows performing multiple operations in a single HTTP request.

client.bucket("blog").collection("posts")
  .batch(batch => {
    batch.deleteRecord("cb0f7b2b-e78f-41a8-afad-92a56f8c88db");
    batch.createRecord({title: "new post", content: "yo"});
    batch.createRecord({title: "another", content: "yo again"});
  })
  .then(result => ...);

Sample result:

[
  {
    "status": 200,
    "path": "/v1/buckets/blog/collections/posts/records/a89dd4b2-d597-4192-bc2b-834116244d29",
    "body": {
      "data": {
        "deleted": true,
        "last_modified": 1456184078090,
        "id": "a89dd4b2-d597-4192-bc2b-834116244d29"
      }
    },
    "headers": {
      "Content-Length": "99",
      "Content-Type": "application/json; charset=UTF-8",
      "Access-Control-Expose-Headers": "Retry-After, Content-Length, Alert, Backoff"
    }
  },
  {
    "status": 201,
    "path": "/v1/buckets/blog/collections/posts/records",
    "body": {
      "data": {
        "content": "yo",
        "last_modified": 1456184078096,
        "id": "afd650b3-1625-42f6-8994-860e52d39201",
        "title": "new post"
      },
      "permissions": {
        "write": [
          "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
        ]
      }
    },
    "headers": {
      "Content-Length": "221",
      "Content-Type": "application/json; charset=UTF-8",
      "Access-Control-Expose-Headers": "Retry-After, Content-Length, Alert, Backoff"
    }
  },
  {
    "status": 201,
    "path": "/v1/buckets/blog/collections/posts/records",
    "body": {
      "data": {
        "content": "yo again",
        "last_modified": 1456184078102,
        "id": "22c1319e-7b09-46db-bec4-c240bdf4e3e9",
        "title": "another"
      },
      "permissions": {
        "write": [
          "basicauth:0f7c1b72cdc89b9d42a2d48d5f0b291a1e8afd408cc38a2197cdf508269cecc8"
        ]
      }
    },
    "headers": {
      "Content-Length": "226",
      "Content-Type": "application/json; charset=UTF-8",
      "Access-Control-Expose-Headers": "Retry-After, Content-Length, Alert, Backoff"
    }
  }
]

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;
  • aggregate: Produces an aggregated result object, grouped by operation types; the result object has the following structure:
{
  "errors":    [], // Encountered errors (HTTP 400, >=500)
  "published": [], // Successfully published resources (HTTP 200, 201)
  "conflicts": [], // Conflicting resources (HTTP 412)
  "skipped":   []  // Missing target resources on the server (HTTP 404)
}

Options

Both bucket() and collection() methods accept an options object as a second arguments where you can define the following options:

  • {Object} headers: Custom headers to send along the request;
  • {Boolean} safe: Ensure safe transactional operations; read more about that below.

Sample usage:

client.bucket("blog", {
  headers: {"X-Hello": "Hello!"},
  safe: true
});

Here the X-Hello header and the safe option will be used for building every outgoing request sent to the server, for every collection attached to this bucket.

This works at the collection level as well:

client.bucket("blog")
  .collection("posts", {
    headers: {"X-Hello": "Hello!"},
    safe: true
  });

Every request sent for this collection will have the options applied.

Last, you can of course pass these options at the atomic operation level:

client.bucket("blog")
  .collection("posts")
  .updateRecord(updatedRecord, {
    headers: {"X-Hello": "Hello!"},
    safe: true
  });

The cool thing being you can always override the default defined options at the atomic operation level:

client.bucket("blog", {safe: true})
  .collection("posts")
  .updateRecord(updatedRecord, {safe: false});

The safe option explained

The safe option can be used:

  • when creating or updating a resource, to ensure that any already existing record matching the provided ID won't be overridden if it exists on the server;
  • when updating or deleting a resource, to ensure it won't be overridden remotely if it has changed in the meanwhile on the server (requires a last_modified value to be provided).

Safe creations

When creating a new ressource, using the safe option will ensure the resource will be created only if it doesn't already exist on the server.

Safe updates

If a last_modified property value is set in the resource object being updated, the safe option will ensure it won't be overriden if it's been modified on the server since that last_modified timestamp, raising an HTTP 412 response describing the conflict when that happens:

const updatedRecord = {
  id: "fbd2a565-8c10-497a-95b8-ce4ea6f474e1",
  title: "new post, modified",
  content: "yoyo",
  last_modified: 1456184189160
};

client.bucket("blog")
  .collection("posts")
  .updateRecord(updatedRecord, {safe: true});

If this record has been modified on the server already, meaning its last_modified is greater than the one we provide , we'll get a 412 error response.

If no last_modified value is provided at all, a safe update will simply guarantee that an existing resource with the provided ID won't be overriden.

Safe deletions

The same applies for deletions, where you can pass both a safe and last_modified options:

client.bucket("blog")
  .collection("posts")
  .deleteRecord("fbd2a565-8c10-497a-95b8-ce4ea6f474e1", {
    safe: true,
    last_modified: 1456184189160
  });

Events

The KintoClient exposes an events property you can subscribe public events from. That events property implements nodejs' EventEmitter interface.

The backoff event

Triggered when a Backoff HTTP header has been received from the last received response from the server, meaning clients should hold on performing further requests during a given amount of time.

The backoff event notifies what's the backoff release timestamp you should wait until before performing another operation:

const client = new KintoClient();

client.events.on("backoff", function(releaseTime) {
  const releaseDate = new Date(releaseTime).toLocaleString();
  alert(`Backed off; wait until ${releaseDate} to retry`);
});

The deprecated event

Triggered when an Alert HTTP header is received from the server, meaning that a feature has been deprecated; the event argument received by the event listener contains the following deprecation information:

  • type: The type of deprecation, which in ou case is always soft-eol (hard-eol alerts trigger an HTTP 410 Gone error);
  • message: The deprecation alert message;
  • url: The URL you can get information about the related deprecation policy.
const client = new KintoClient();

client.events.on("deprecated", function(event) {
  console.log(event.message);
});

The retry-after event

When an error occurs on server, a Retry-After HTTP header indicates the duration in seconds that clients should wait before retrying the request.

The retry-after event notifies what is the timestamp you should wait until before performing another operation:

const client = new KintoClient();

client.events.on("retry-after", function(releaseTime) {
  const releaseDate = new Date(releaseTime).toLocaleString();
  alert(`Wait until ${releaseDate} to retry`);
});

Note:

Eventually, we would like to automate the retry behaviour for requests. See https://github.com/Kinto/kinto-client/issues/34