# Guide
# Installation
To get started, install vue-postgrest via your package manager:
yarn add vue-postgrest
npm install vue-postgrest
The default export provides the plugin to use in your main.js
. When installing the plugin, you can pass the root URI of your PostgREST server as a plugin option. All requests to the API made by the mixin, component or instance methods will use this URI as base.
import Vue from 'vue'
import VuePostgrest from 'vue-postgrest'
Vue.use(VuePostgrest, {
apiRoot: '/api/v1/'
})
When installed, the plugin registers the postgrest
component globally on your vue instance for use in your templates, as well as several instance methods on vm.$postgrest
. The most convenient way is to use the pg
mixin, though.
WARNING
You have to install the plugin, even if you only use the mixin in your components!
# Retrieving Data
To use the mixin, provide a pgConfig
object on your component instance with the route
option set to the table/view you want to query:
<script>
import { pg } from 'vue-postgrest'
export default {
name: 'HeroesList',
mixins: [pg],
data () {
return {
pgConfig: {
route: 'heroes'
}
}
}
}
</script>
# Column Filtering
To access the data sent by the server, use this.pg
, which is an array holding the server response by default.
<template>
<ul>
<li v-for="hero in pg" :key="hero.id">{{ hero.name }}</li>
</ul>
</template>
Using the mixin option single: true
will set the Accept
header to tell PostgREST to return a single item unenclosed by an array. In this case you can use this.pg
to access the returned item directly.
The mixin option query
is used to construct the PostgREST query string. Use query.select
for column filtering like this:
<script>
...
pgConfig: {
query: {
select: ['id', 'name']
}
}
...
</script>
The select key alternatively accepts an object with column names as keys. You can use aliasing and casting like this:
<script>
...
pgConfig: {
query: {
select: {
id: true,
'fullName:name': true,
'age::text': true
}
}
}
...
</script>
# Ordering
To order your response, you can either pass an array of strings or an object to query.order
. E.g.:
<script>
...
query: {
select: ['*'],
order: ['id.asc', 'name.desc']
}
...
</script>
// or
<script>
...
query: {
select: ['*'],
order: {
id: 'asc',
name: 'desc'
}
}
...
</script>
# Row Filtering
The query
constructs column conditions from it's keys. Operators are dot-appended to the key.
E.g. to use multiple conditions:
<script>
...
query: {
select: ['*'],
'id.in': [1, 2, 3],
'age.gte': 50
}
...
</script>
When passing arrays, the resulting query string is constructed based on the used operator! See Arrays. Furthermore, undefined
values will exclude the column condition from the query string - this can be useful if you create your query object dynamically.
TIP
For convenient creation of range objects see Range Objects!
# Embedding
PostgREST offers an easy way to handle relationships between tables/views. You can leverage this by using the embed syntax in your queries. The syntax to filter, order or select embeds corresponds to the root level of the query object:
<script>
...
query: {
select: {
id: true,
name: true,
superpowers: true,
'cars:vehicles': {
select: '*',
'type.eq': 'car'
},
'id.eq': 1
}
...
</script>
Important: If you omit the select
key in an embed object, it is assumed that you want to access a JSON-field instead of embedding a resource! See JSON Columns for details.
# Loading / Refreshing
For monitoring the current status of the request, you can use this.pg.$get
which is an ObservableFunction. pg.$get.isPending
tells you, if a request is still pending:
<template>
<loading-spinner v-if="pg.$get.isPending"/>
<ul v-else>
<li v-for="hero in pg" :key="hero.id">{{ hero.name }}</li>
</ul>
</template>
You can call the $get
function to rerun the get request, e.g. if you need to refresh your data manually.
Note: The $get
function also exposes getters for information about failed requests, see error handling.
# Pagination
Server side pagination can be achieved by setting the mixin options limit
and offset
. When used as a mixin option (or component prop), these options set the appropriate request headers automatically. When used inside a query object, limit and offset will be appended to the query string.
Range
To get information about the paginated response, the mixin provides the pg.$range
object, based on the response's Content-Range
header. To get the total count of available rows, use the mixin option count = 'exact'
which sets the corresponding Prefer
header.
<template>
...
<span>Displaying hero no. {{ pg.$range.first }} to {{ pg.$range.last }} of {{ pg.$range.totalCount }} total Heroes</span>
<button @click="offset += 20">Next 20 Heroes</button>
<button @click="offset -= 20" :disabled="offset - 20 < 0">Previous 20 Heroes</button>
...
</template>
<script>
...
data () {
return {
offset: 0
}
},
computed: {
pgConfig () {
return {
route: 'heroes',
query: {
select: ['*'],
vehicles: {
select: ['*'],
limit: 5
}
},
limit: 20,
offset: this.offset
}
}
}
}
</script>
# Multiple Requests
Sometimes it may be necessary to access multiple tables/views or query the same route twice the from the same component. You can use the postgrest
component for this.
The component takes the same options as the pg
mixin as props and provides it's scope as slot props, so you can use it in your template like this:
<template>
<!-- fetch and display the latest entries -->
<div class="heroes-list">
<postgrest
route="heroes"
query = "{
select: ['name'],
order: {
'created_at': 'desc'
}
}"
:limit="2"
/>
<template #default="items">
<loading-spinner v-if="items.$get.isPending"/>
<div v-else>
Our new heroes are {{ heroes[0].name }} and {{ heroes[1].name }}!
</div>
</template>
</postgrest>
<!-- display the heroes list fetched by the mixin -->
<loading-spinner v-if="pg.$get.isPending"/>
<ul v-else>
<li v-for="hero in pg" :key="hero.id">{{ hero.name }}</li>
</ul>
</div>
</template>
Note: If you encounter situations where it is more convenient to do this programmatically, you can also use instance methods! The this.$postgrest
exposes a Route for each table/view that is available in your schema. You could then rewrite the above example like this:
<template>
<div class="heroes-list">
<!-- display the latest entries -->
<div v-if="latestHeroes.length">
Our new heroes are {{ latestHeroes[0].name }} and {{ latestHeroes[1].name }}!
</div>
<!-- display the heroes list fetched by the mixin -->
...
</div>
</template>
<script>
...
data () {
return {
latestHeroes: []
}
},
computed: {
pgConfig () {
...
},
heroes () {
return this.pg
}
},
async mounted () {
// wait for schema to be loaded, so the routes are available
await this.$postgrest.$ready
// fetch the latest two new heroes
const resp = await this.$postgrest.heroes.get({
select: ['name'],
order: {
'created_at': 'desc'
}
}, {
limit: 2
})
// convert fetch response to json
this.latestHeroes = await resp.json()
}
}
</script>
....
# Modifying Data
Each item provided by the mixin or the component is a Generic Model, which is a wrapper for the entity received from the server with some added methods and getters.
Getting an item, modifying it's data and patching it on the server can be as simple as:
<template>
...
<input type="text" v-model="hero.name" @blur="hero.$patch"/>
...
</template>
<script>
...
computed: {
pgConfig () {
return {
route: 'heroes',
query: {
'id.eq': 1
},
single: true
}
},
hero () {
return this.pg
}
...
</script>
WARNING
The instance methods $postgrest.ROUTE.METHOD
do not wrap the response in GenericModels but return the fetch Response
directly.
# Model State
Just like the mixin method pg.$get
, the request-specific methods provided by a GenericModel are ObservableFunctions. This means, you can check on the status of pending requests or errors via the respective getters. In addition, GenericModels provide the getter model.$isDirty
, which indicates if the model's data changed from it's initial state, as well as a model.$reset()
method, which resets the data to it's initial state.
Note: The model is updated after $patch requests by default and initial state
is set to the updated data. If you don't want to update the model, e.g. when doing a partial patch, set the $patch
option return='minimal'
.
The first argument to the model.$patch
method is an options object. The second argument to $patch
is an object with additional patch data. See $patch for details.
A more extensive example could look like this:
<template>
...
<div v-if="hero in heroes" :key="hero.id">
<input :class="{ dirty: hero.$isDirty }" type="text" v-model="hero.name"/>
<button @click="powerUp(hero)">Make Superhero!</button>
<button @click="patch(hero)">Update</button>
<button @click="delete(hero)">Delete</button>
<button @click="hero.$reset">Reset</button>
</div>
...
</template>
<script>
...
computed: {
pgConfig () {
return {
route: 'heroes',
query: {}
}
},
heroes () {
return this.pg
},
methods () {
async patch (hero) {
await hero.$patch({ columns: ['name'] })
},
async powerUp (hero) {
await hero.$patch({}, { superhero: true })
},
async delete (hero) {
await hero.$delete()
// refresh the heroes list after delete
this.pg.$get()
}
}
...
</script>
Using the postgrest
component and it's slot scope for patching:
<template>
...
<postgrest
route="heroes"
query = "{
select: ['id', name'],
'age.gt': 25
}"
/>
<template #default="heroes">
<loading-spinner v-if="heroes.$get.isPending"/>
<div v-else>
<div v-for="hero in heroes" :key="hero.id">
<input :class="{ dirty: hero.$isDirty }" type="text" v-model="hero.name"/>
<button @click="update(hero)">Update</button>
</div>
</div>
</template>
</postgrest>
...
</template>
<script>
...
methods: {
async update (item) {
await item.$patch({}, { 'updated_by': this.$store.getters.userId })
}
}
...
</script>
# Creating Models
When the mixin option single
is false
(the default) this.pg
is actually an instance of a GenericCollection. The GenericCollection
has a method pg.$new(...)
to create new GenericModel
s. You can then call $post()
on the returned models to make a POST
request.
<template>
...
<input type="text" v-model="newItem.name"/>
<button @click="post"/>Create new Hero</button>
...
</template>
<script>
...
data () {
return {
newItem: null
}
},
computed: {
pgConfig () {
return {
route: 'heroes'
}
}
},
mounted () {
this.newItem = this.pg.$new({ name: 'New Hero' })
},
methods: {
async post () {
// setting return to minimal, so the newItem is not updated with the server response and can be reset to our mounted template later
await this.newItem.$post({ return: 'minimal' })
// resetting newItem to our initial template
this.newItem.$reset()
}
}
...
</script>
# Upserts
You can do an upsert (insert or update, when it already exists) with either a POST
or a PUT
request. To make a PUT
request, just call $put()
on the model - but make sure to set the primary key on the model first.
Alternatively, you can use all options that a route offers with $post()
. To perform an upsert, you can pass the resolution
option, which sets the resolution part of the Prefer
header. To set the on_conflict
query string parameter, see Query.
Example for $post()
upsert:
<template>
...
<input type="text" v-model="newHero.name"/>
<button @click="post"/>Create Hero</button>
...
</template>
<script>
...
data () {
return {
newHero: null
}
},
computed: {
pgConfig () {
return {
route: 'heroes'
}
}
},
mounted () {
this.newHero = this.pg.$new({ name: 'New Hero' })
},
methods: {
async post () {
await this.newHero.$post({ resolution: 'merge-duplicates', query: { on_conflict: 'name' } })
}
}
...
</script>
# Handling Errors
# Mixin / Component
The mixin calls the onError
hook on your component instance whenever a FetchError or an AuthError is thrown. To react to errors from the postgrest
component, use the error
event. The error object is passed to the hook/event.
# GenericModel / Instance Methods / Stored Procedures
All request-specific methods from a GenericCollection or GenericModel, as well as the instance methods and stored procedure calls throw AuthError and FetchError. Additionally, the generic model methods throw PrimaryKeyError.
TIP
You can test whether a schema was found for the base URI by catching SchemaNotFoundError on $postgrest.$ready.
# Full Example
<template>
<div>
<postgrest
route="vehicles"
query="{}"
@error="handleError">
<div #default="items">
<span v-if="items.$get.hasErrors">Could not load vehicles...</span>
<div v-else>
<div v-for="item in items" :key="item.id">
<input type="text" v-model="item.type" @blur="updateVehicle(item)"/>
</div>
</div>
</div>
</postgrest>
<button @click="deleteHero">Delete Hero!</button>
<button @click="destroyPlanets">Destroy all Planets!</button>
</div>
</template>
<script>
import { pg, AuthError, FetchError } from 'vue-postgrest'
export default {
name: 'Component',
mixins: [pg],
data () {
return {
pgConfig: {
route: 'heroes',
query: {
'id.eq': 1
},
single: true
}
}
},
// mixin hook
onError (err) {
this.handleError(err)
},
methods: {
async destroyPlanets () {
try {
await this.$postgrest.rpc.destroyAllPlanets()
} catch (e) {
this.handleError(e)
}
},
async deleteHero () {
try {
await this.pg.$delete()
} catch (e) {
this.handleError(e)
}
},
async updateVehicle (item) {
try {
await item.$patch()
} catch (e) {
this.handleError(e)
}
},
handleError (err) {
if (err instanceof AuthError) {
// handle token error
// AuthError is thrown when PostgREST rejects the token
} else if (err instanceof FetchError) {
// handle error from fetch
// FetchError is thrown when the response status code is >= 400
}
}
}
}
</script>
# Stored Procedures
For calling stored procedures, the instance method $postgrest.rpc
is provided. On loading the schema, all available stored procedures are registered here. The stored procedure call accepts an object containing the parameters that are passed to the stored procedure and an options object. By default, RPCs are called with the request method POST
, you can set the rpc option get: true
if you want to call a RPC with GET
instead. For setting the Accept
header, use the option accept
.
<script>
export default {
name: 'Component',
methods: {
async destroyAllPlanets () {
// wait till schema is loaded
await this.$postgrest.$ready
const result = await this.$postgrest.rpc.destroyplanets({ countdown: false }, {
accept: 'text',
headers: { 'Warning': 'Will cause problems!' }
})
if (await result.text() !== 'all gone!') {
this.$postgrest.rpc.destroyplanets({ force: true })
}
}
}
}
</script>
TIP
If you want to call a RPC before the schema is loaded, you can call $postgrest.rpc
directly by passing the name of the stored procedure that should be called as the first argument, followed by the rpc parameters and options. See RPC for details.
# Authentication
The most convenient way to set the Authorization
header to include your jwt token is to use the setDefaultToken method exported by the module. This method sets the token to use for all subsequent communication with the PostgREST server.
import { setDefaultToken } from 'vue-postgrest'
<script>
name: 'App',
mounted () {
setDefaultToken(this.$store.getters.authToken)
}
}
</script>
If you want to overwrite the token used for specific requests, you can either use the mixin option token
or the component prop, respectively.
To handle rejected requests due to token errors, use the AuthError
that is thrown when the server rejects your token, see Handling Errors for details.
TIP
You can use the instance method $postgrest
to create a new schema. If you set the first argument (apiRoot
) to undefined, a new schema is created with the base URI used by the default schema. You can pass an auth token as the second argument, which will then be used for subsequent requests with the new schema.