For the Go version please refer to /v023upgrade/go.
Upgrade overview
Please note that you don't have to upgrade to v0.23.0 if you are not planning further developing your existing app or are satisfied with the v0.22.x features set. There are no critical issues with PocketBase v0.22.x and in the case of reported bugs and security vulnerabilities, the fixes will be backported for at least until Q1 of 2025 (if not longer).
If you don't plan upgrading just make sure to pin the SDKs version to their latest PocketBase v0.22.x compatible:
- JS SDK:
<0.22.0
- Dart SDK:
<0.19.0
Before upgrading make sure to backup your current pb_data
directory somewhere safe.
PocketBase v0.23+ comes with many changes and requires applying manual migration steps for the
pb_hooks
and pb_migrations
.
Existing data in pb_data/data.db
will be automatically upgraded once the PocketBase v0.23.0
executable is started.
But there is no easy and automated code upgrade solution so be prepared because it might take you some time
- from 1h to entire weekend!
Suggested upgrade path:
- Download a backup of your production
pb_data
so that you can adjust your code and test the upgrade locally. - Ensure that you are already using PocketBase v0.22.x (you can find the version number in the
bottom-right corner in the UI). If you are using an older version, please upgrade first to
PocketBase v0.22.x
(there are no major breaking changes in the recent v0.22.x releases so it should be enough to download
the specific executable and run it against your
pb_data
). - If you are using the SDKs, apply the necessary changes to your client-side code by following:
- If your existing
pb_migrations
files are all autogenerated, you may find it easier to upgrade by deleting all of them and run with the PocketBase v0.23.0 executable./pocketbase migrate collections
which will create a full snapshot of your latest collections configuration.
Optionally, after that you can also run./pocketbase migrate history-sync
to remove the deleted migrations from the applied migrations history. - Apply the necessary
pb_hooks
code changes based on the notes listed below in this document. - Start PocketBase v0.23.0 as usual and test your local changes (including a test against no existing pb_data).
- If everything is working fine, create another more recent production backup for just in case.
After that you can deploy your PocketBase v0.23.0 executable, together with the upgraded client-side code,pb_hooks
andpb_migrations
to your production server.
The below listed changes doesn't cover everything but it should be a good starting point.
If you need help with the "translation" of your old code to the new APIs, feel free to open a Q&A discussion with the specific code sample and I'll try to assist you.
Initialisms case normalization
For consistency with the Go standard library and to silent commonly used linters the following initilisms case normalization was applied for all exported bindings:
Old | New |
---|---|
%Json% | %JSON% |
%Ip% | %IP% |
%Url% | %URL% |
%Smtp% | %SMTP% |
%Tls% | %TLS% |
%Jwt% | %JWT% |
App changes
The Dao
abstraction has been removed and most of the old Dao
methods are now part of the $app
instance.
The app settings related to the OAuth2 and email templates are moved in the collection options to allow more granular customizations.
For more details about all new app fields and methods, please refer to the
$app.*
docs in the
JSVM reference.
$app.cache()
$app.store()
$app.refreshSettings()
$app.reloadSettings()
Admin DB methods (admins are now system "_superusers" auth collection records)
$app.dao().adminQuery()
$app.recordQuery("_superusers")
$app.dao().findAdminById(id)
$app.findRecordById("_superusers", id)
$app.dao().findAdminByEmail(email)
$app.findAuthRecordByEmail("_superusers", email)
$app.dao().findAdminByToken(token, baseTokenKey)
// tokenType could be:
// - auth
// - file
// - verification
// - passwordReset
// - emailChange
$app.findAuthRecordByToken(token, [tokenType])
$app.dao().totalAdmins()
$app.countRecords("_superusers", [exprs...])
$app.dao().deleteAdmin(admin)
$app.delete(admin)
$app.dao().saveAdmin(admin)
$app.save(admin)
Record DB methods
$app.dao().recordQuery(collectionModelOrIdentifier)
$app.recordQuery(collectionModelOrIdentifier)
$app.dao().findRecordById(collectionNameOrId, recordId, [optFilters...])
$app.findRecordById(collectionModelOrIdentifier, recordId, [optFilters...])
$app.dao().findRecordsByIds(collectionNameOrId, recordIds, [optFilters...])
$app.findRecordsByIds(collectionModelOrIdentifier, recordIds, [optFilters...])
$app.dao().findRecordsByExpr(collectionNameOrId, [exprs...])
$app.findAllRecords(collectionModelOrIdentifier, [exprs...])
$app.dao().findFirstRecordByData(collectionNameOrId, key, value)
$app.findFirstRecordByData(collectionModelOrIdentifier, key, value)
$app.dao().findRecordsByFilter(collectionNameOrId, filter, sort, limit, offset, [params])
app.findRecordsByFilter(collectionModelOrIdentifier, filter, sort, limit, offset, [params])
N/A
$app.countRecords(collectionModelOrIdentifier, [exprs...])
$app.dao().findAuthRecordByToken(token, baseTokenKey)
// tokenType could be:
// - auth
// - file
// - verification
// - passwordReset
// - emailChange
$app.findAuthRecordByToken(token, [tokenType])
$app.dao().findAuthRecordByEmail(collectionNameOrId, email)
$app.findAuthRecordByEmail(collectionModelOrIdentifier, email)
$app.dao().findAuthRecordByUsername(collectionNameOrId, username)
// The "username" field is no longer required and has no special meaning.
$app.findFirstRecordByData(collectionModelOrIdentifier, "username", username)
$app.dao().suggestUniqueAuthRecordUsername(collectionNameOrId, baseUsername, [excludeIds...])
// The "username" field is no longer required and has no special meaning. As a workaround you can use something like:
function suggestUniqueAuthRecordUsername(
collectionModelOrIdentifier,
baseUsername,
) {
let username = baseUsername
for (i = 0; i < 10; i++) { // max 10 attempts
try {
let total = $app.countRecords(
collectionModelOrIdentifier,
$dbx.exp("LOWER([[username]])={:username}", {"username": username.toLowerCase()}),
)
if (total == 0) {
break // already unique
}
} catch {}
username = baseUsername + $security.randomStringWithAlphabet(3+i, "123456789")
}
return username
}
$app.dao().canAccessRecord(record, requestInfo, accessRule)
$app.canAccessRecord(record, requestInfo, accessRule)
$app.dao().expandRecord(record, expands, optFetchFunc)
$app.expandRecord(record, expands, optFetchFunc)
$app.dao().expandRecords(records, expands, optFetchFunc)
$app.expandRecords(records, expands, optFetchFunc)
$app.dao().saveRecord(record)
$app.save(record)
$app.dao().deleteRecord(record)
$app.delete(record)
ExternalAuth DB methods (converted to "_externalAuths" collection records)
$app.dao().findAllExternalAuthsByRecord(authRecord)
$app.findAllExternalAuthsByRecord(authRecord)
N/A
$app.findAllExternalAuthsByCollection(collection)
$app.dao().findFirstExternalAuthByExpr(expr)
$app.findFirstExternalAuthByExpr(expr)
$app.dao().findExternalAuthByRecordAndProvider(authRecord)
$app.findFirstExternalAuthByExpr($dbx.hashExp({
"collectionRef": authRecord.collection().id,
"recordRef": authRecord.id,
"provider": provider,
}))
$app.dao().deleteExternalAuth(model)
$app.delete(model)
$app.dao().saveExternalAuth(model)
$app.save(model)
Collection DB methods
$app.dao().collectionQuery()
$app.collectionQuery()
$app.dao().findCollectionsByType(collectionType)
$app.findAllCollections(collectionTypes...)
$app.dao().findCollectionByNameOrId(nameOrId)
$app.findCollectionByNameOrId(nameOrId)
$app.dao().isCollectionNameUnique(name, excludeIds...)
$app.isCollectionNameUnique(name, excludeIds...)
$app.dao().findCollectionReferences(collection, excludeIds...)
$app.findCollectionReferences(collection, excludeIds...)
$app.dao().deleteCollection(collection)
$app.delete(collection)
$app.dao().saveCollection(collection)
$app.save(collection)
$app.dao().importCollections(importedCollections, deleteMissing, afterSync)
$app.importCollections(importSliceOfMap, deleteMissing)
N/A
$app.importCollectionsByMarshaledJSON(sliceOfMapsBytes, deleteMissing)
$app.dao().syncRecordTableSchema(newCollection, oldCollection)
$app.syncRecordTableSchema(newCollection, oldCollection)
Log DB methods
$app.dao().logQuery()
$app.logQuery()
$app.dao().findLogById(id)
$app.findLogById(id)
$app.dao().logsStats(expr)
$app.logsStats(expr)
$app.dao().deleteOldLogs(createdBefore)
$app.deleteOldLogs(createdBefore)
$app.dao().saveLog(log)
$app.auxSave(log)
Other DB methods
$app.dao().saveSettings(newSettings, [optEncryptionKey])
$app.save(newSettings)
const titles = ["title1", "title2", "title3"]
const collection = $app.dao().findCollectionByNameOrId("articles")
$app.dao().runInTransaction((txDao) => {
// create new record for each title
for (let title of titles) {
const record = new Record(collection)
record.set("title", title)
txDao.saveRecord(record)
}
})
const titles = ["title1", "title2", "title3"]
const collection = $app.findCollectionByNameOrId("articles")
$app.runInTransaction((txApp) => {
// create new record for each title
for (let title of titles) {
const record = new Record(collection)
record.set("title", title)
txApp.saveRecord(record)
}
})
$app.dao().vacuum()
$app.vacuum() / $app.auxVacuum()
$app.dao().hasTable(tableName)
$app.hasTable(tableName)
$app.dao().tableColumns(tableName)
$app.tableColumns(tableName)
$app.dao().tableInfo(tableName)
$app.tableInfo(tableName)
$app.dao().tableIndexes(tableName)
$app.tableIndexes(tableName)
$app.dao().deleteTable(tableName)
$app.deleteTable(tableName)
$app.dao().deleteView(name)
$app.deleteView(name)
$app.dao().saveView(name, selectQuery)
$app.saveView(name, selectQuery)
$app.dao().createViewSchema(selectQuery)
$app.createViewFields(selectQuery)
Record model changes
The Record create/update operations are simplified and the Dao
and
RecordUpsertForm
abstractions have been removed.
$app.save(record)
both validate and persist the record. Use
$app.saveNoValidate(record)
if you want to skip the validations.
Files are treated as regular field value, so for uploading a new record file is enough to call
record.set("yourFileField", $filesystem.fileFromPath("/path/to/file1"))
.
Additionally, the record.set()
method now supports all field modifiers that are available
with the Web APIs. For example:
record.set("total+", 10)
adds 10
to the existing total
record field
value. More information about the available modifiers you can find in the main documentation.
RecordUpsertForm migration
const record = $app.dao().findRecordById("articles", "RECORD_ID")
const form = new RecordUpsertForm($app, record)
form.loadData({
"title": "Lorem ipsum",
"active": true,
})
// upload file(s)
const f1 = $filesystem.fileFromPath("/path/to/file1")
const f2 = $filesystem.fileFromPath("/path/to/file2")
form.addFiles("documents", f1, f2)
// mark file(s) for deletion
form.removeFiles("featuredImages", "demo_xzihx0w.png")
form.submit()
const record = $app.findRecordById("articles", "RECORD_ID")
// there is also record.load({ ... })
record.set("title", "Lorem ipsum")
record.set("active", true)
// upload file(s)
record.set("documents", [
$filesystem.fileFromPath("/path/to/file1"),
$filesystem.fileFromPath("/path/to/file2"),
])
// mark file(s) for deletion
record.set("featuredImages-", "demo_xzihx0w.png")
$app.save(record)
Model changes
record.schemaData()
record.fieldsData()
record.unknownData()
record.customData()
record.withUnknownData(state)
record.withCustomData(state)
record.originalCopy()
record.original()
record.cleanCopy()
record.fresh()
record.getTime(fieldName)
record.getDateTime(fieldName).Time()
$tokens.* merge
$tokens.recordAuthToken($app, record)
record.newAuthToken()
$tokens.recordVerifyToken($app, record)
record.newVerificationToken()
$tokens.recordResetPasswordToken($app, record)
record.newPasswordResetToken()
$tokens.recordChangeEmailToken($app, record, newEmail)
record.newEmailChangeToken()
$tokens.recordFileToken($app, record)
record.NewFileToken()
Collection model changes
Added new dedicated collection type constructors:
new BaseCollection(name)
new AuthCollection(name)
new ViewCollection(name)
Collection.schema
is now a regular array of fields and the model field is renamed to
Collection.fields
.
There are now also dedicated typed fields bindings equivalent to the Go ones as an alternative to
new Field({type: "...", ...})
:
new NumberField({ ... })
new BoolField({ ... })
new TextField({ ... })
new URLField({ ... })
new EmailField({ ... })
new EditorField({ ... })
new DateField({ ... })
new AutodateField({ ... })
new JSONField({ ... })
new RelationField({ ... })
new SelectField({ ... })
new FileField({ ... })
All previous dynamic Collection.options.*
fields are flattened for consistency with the JSON Web
APIs:
collection.options.query = "..."
collection.viewQuery = "..."
collection.options.manageRule = "..."
collection.manageRule = "..."
collection.options.onlyVerified = true
collection.authRule = "verified = true"
collection.options.onlyVerified = true
collection.authRule = "verified = true"
collection.options.allowOAuth2Auth = true
// note: providers can be set via collection.oauth2.providers
collection.oauth2.enabled = true
collection.options.allowUsernameAuth = true
collection.passwordAuth = {
"enabled": true,
identityFields: ["username"],
}
collection.options.allowEmailAuth = true
collection.passwordAuth = {
"enabled": true,
identityFields: ["email"],
}
collection.options.requireEmail = true
collection.options.exceptEmailDomains = ["example.com"]
collection.options.onlyEmailDomains = ["example.com"]
// you can adjust the system "email" field options in the same way as the regular Collection.fields, e.g.:
collection.fields.getByName("email").require = true
collection.fields.getByName("email").exceptDomains = ["example.com"]
collection.fields.getByName("email").onlyDomains = ["example.com"]
collection.options.minPasswordLength = 10
// you can adjust the system "password" field options in the same way as the regular Collection.fields. e.g.:
collection.fields.getByName("password").min = 10
The Collection create/update operations are also simplified and the Dao
and
CollectionUpsertForm
abstractions have been removed.
$app.save(collection)
both validate and persist the collection. Use
$app.saveNoValidate(collection)
if you want to skip the validations.
Below is an example CollectionUpsertForm
migration:
const collection = new Collection()
const form = new CollectionUpsertForm($app, collection)
form.name = "example"
form.type = "base"
form.listRule = null
form.viewRule = "@request.auth.id != ''"
form.createRule = ""
form.updateRule = "@request.auth.id != ''"
form.deleteRule = null
form.schema.addField(new SchemaField({
name: "title",
type: "text",
required: true,
options: {
max: 10,
},
}))
form.schema.addField(new SchemaField({
name: "users",
type: "relation",
options: {
maxSelect: 5,
collectionId: "ae40239d2bc4477",
cascadeDelete: true,
},
}))
form.submit()
const collection = new BaseCollection("name")
collection.listRule = null
collection.viewRule = "@request.auth.id != ''"
collection.createRule = ""
collection.updateRule = "@request.auth.id != ''"
collection.deleteRule = null
collection.fields.add(new TextField({
name: "title",
required: true,
max: 10,
}))
collection.fields.add(new RelationField({
name: "users",
maxSelect: 5,
collectionId: "ae40239d2bc4477",
cascadeDelete: true,
}))
$app.save(collection)
Migration changes
If your existing migrations are all autogenerated, you may find it easier to upgrade by deleting
all of them and run with the PocketBase v0.23.0 executable
./pocketbase migrate collections
which will create a full snapshot of your latest collections configuration.
Optionally, after that you can also run ./pocketbase migrate history-sync
to remove the
deleted migrations from the applied migrations history.
The migration
function accept the transactional app instance as argument instead of
dbx.Builder
. This allow access not only to the database but also to the app settings,
filesystem, mailer, etc.
migrate((db) => {
const dao = new Dao(db);
const settings = dao.findSettings()
settings.meta.appName = "test"
settings.logs.maxDays = 2
dao.saveSettings(settings)
})
migrate((app) => {
const settings = app.settings()
settings.meta.appName = "test"
settings.logs.maxDays = 2
app.save(settings)
})
Routing changes
PocketBase v0.23.0 ships with a new router built on top of the standard
Go 1.22 mux enhancements
.
Some of the more notable differences are:
- Route path parameters must comply with the
Go 1.22 mux syntax
.
Or in other words:param
must be replaced with{param}
. For static path parameter instead of*
you have to use special{path...}
param. - Similar to the new hook handlers, middlewares must call
e.next()
in order to proceed with the execution chain. - To access the auth state in a route you can call
e.auth
. Similarly, to set a new logged auth state you can simply assign a new auth record to thee.auth
field. - Because admins are now
_superusers
auth collection records,e.auth
will be set to the authenticated superuser.
To check whethere.auth
is a superuser you can use thee.hasSuperuserAuth()
helper (or alternatively check the record methode.auth.isSuperuser()
ore.auth.collection().name == "_superusers"
). $apis.requestInfo(c)
has been replaced withe.requestInfo()
.
Note also that therequestInfo.data
field was renamed torequestInfo.body
for consistency with the@request.body.*
API rules change.
Below you can find a short comparison table with the new routing interface:
Route registration
routerAdd("GET", "/hello/:name", (c) => {
let name = c.pathParam("name")
return c.json(200, { "message": "Hello " + name })
}, $apis.requireRecordAuth())
routerAdd("GET", "/hello/{name}", (e) => {
let name = e.request.pathValue("name")
return e.json(200, { "message": "Hello " + name })
}, $apis.requireAuth())
Static files serving
routerAdd("GET", "/*", $apis.staticDirectoryHandler("/path/to/public", false))
routerAdd("GET", "/{path...}", $apis.static("/path/to/public", false))
Middleware registration
routerUse((next) => {
const header = c.request().header.get("Some-Header")
if (!header) {
throw new BadRequestError("Invalid request")
}
return (c) => {
return next(c)
}
})
routerUse((e) => {
const header = e.request.header.get("Some-Header")
if (!header) {
throw new BadRequestError("Invalid request")
}
return e.next();
})
Retrieving the current auth state
// "c" is the echo httpContext
const admin = c.get("admin") // admin model
const record = c.get("authRecord") // auth record model
// you can also read the auth state from the cached request info
const info = $apis.requestInfo(c)
const admin = info.admin
const record = info.authRecord
const isAdminAuth = admin != null
// "e" is the request event
const auth = e.auth // auth record model
// you can also read the auth state from the cached request info
const info = e.requestInfo()
const auth = info.auth
const isSuperuserAuth = auth?.isSuperuser() // or e.hasSuperuserAuth()
Reading path parameters
const id = c.pathParam("id")
const id = e.request.pathValue("id")
Reading query parameters
const search = c.queryParam("search")
// or via the cached request object
const search = $apis.requestInfo(c).query.search
const search = e.request.url.query().get("test")
// or via the cached request object
const search = e.requestInfo().query.search
Reading request headers
const token = c.request().header.get("Some-Header")
// or via the cached request object (the header value is always normalized)
const token = $apis.requestInfo(c).headers["some_header"]
const token = e.request.header.get("Some-Header")
// or via the cached request object (the header value is always normalized)
const token = e.requestInfo().headers["some_header"]
Writing response headers
c.response().header().set("Some-Header", "123")
e.response.header().set("Some-Header", "123")
Reading request body
// read/scan the request body fields into a typed object
// (note that the echo request body cannot be read twice with "bind")
const body = new DynamicModel({
title: "",
})
c.bind(body)
console.log(body.title)
// read the body via the cached request object
const body = $apis.requestInfo(c).data
console.log(body.title)
// read single multipart/form-data field
const title = c.formValue("title")
// read single multipart/form-data file
const doc = c.formFile("document")
// convert the multipart/form-data file into Array<$filesystem.File>
const doc2 = [$filesystem.fileFromMultipart(doc)]
// read/scan the request body fields into a typed object
// (it safe to be read multiple times)
const body = new DynamicModel({
title: "",
})
e.bindBody(body)
console.log(body.title)
// read the body via the cached request object
const body = e.requestInfo().body
console.log(body.title)
// read single multipart/form-data field
const title = e.request.formValue("title")
// read single multipart/form-data file
const doc = e.request.formFile("document")
// return the uploaded files as Array<$filesystem.File>
const doc2 = e.findUploadedFiles("document")
Writing response body
// send response with json body
c.json(200, {"name": "John"})
// send response with string body
c.string(200, "Lorem ipsum...")
// send response with html body
// (check also the "Rendering templates" section)
c.html(200, "<h1>Hello!</h1>")
// redirect
c.redirect(307, "https://example.com")
// send response with no body
c.noContent(204)
// send response with json body
e.json(200, {"name": "John"})
// send response with string body
e.string(200, "Lorem ipsum...")
// send response with html body
// (check also the "Rendering templates" section)
e.html(200, "<h1>Hello!</h1>")
// redirect
e.redirect(307, "https://example.com")
// send response with no body
e.noContent(204)
Builtin middlewares
$apis.requireRecordAuth([optCollectionNames...])
$apis.requireAuth([optCollectionNames...])
$apis.requireAdminAuth()
// the same as $apis.requireAuth("_superusers")
$apis.requireSuperuserAuth()
$apis.requireAdminAuthOnlyIfAny($app)
N/A
$apis.requireAdminOrRecordAuth("users")
$apis.requireAuth("_superusers", "users")
$apis.requireAdminOrOwnerAuth(ownerIdParam)
$apis.requireSuperuserOrOwnerAuth(ownerIdParam)
$apis.activityLogger(app)
// No longer needed because it is registered by default for all routes.
// If you want to disable the log of successful route execution you can attach the $apis.skipSuccessActivityLog() middleware.
API helpers
$apis.recordAuthResponse($app, c, record)
$apis.recordAuthResponse(e, record)
$apis.enrichRecord(c, $app.dao(), record, "categories")
$apis.enrichRecords(c, $app.dao(), records, "categories")
$apis.enrichRecord(e, record, "categories")
$apis.enrichRecords(e, records, "categories")
Event hooks changes
For more details about all new hooks and their event properties, please refer to the
on*
method docs in the
JSVM reference.
onBeforeBootstrap -> onBootstrap
onBeforeBootstrap((e) => {
// ...
})
onBootstrap((e) => {
// ...
e.next()
})
onAfterBootstrap -> onBootstrap
onAfterBootstrap((e) => {
// ...
})
onBootstrap((e) => {
e.next()
// ...
})
onBeforeApiError/onAfterApiError -> any middleware
onBeforeApiError((e) => {
// ...
})
routerUse((e) => {
try {
e.next()
} catch (err) {
// ...
}
})
onTerminate
onTerminate((e) => {
// ...
})
onTerminate((e) => {
// ...
e.next()
})
onModelBeforeCreate -> onRecordCreate
onModelBeforeCreate((e) => {
// e.model ...
})
// note: onModelCreate is available if you are targeting non-Record/Collection models
onRecordCreate((e) => {
// e.record ...
e.next()
})
onModelAfterCreate -> onRecordAfterCreateSuccess
onModelAfterCreate((e) => {
// e.model ...
})
// note: onModelAfterCreateSuccess is available if you are targeting non-Record/Collection models
onRecordAfterCreateSuccess((e) => {
// e.record ...
e.next()
})
onModelBeforeUpdate -> onRecordUpdate
onModelBeforeUpdate((e) => {
// e.model ...
})
// note: onModelUpdate is available if you are targeting non-Record/Collection models
onRecordUpdate((e) => {
// e.record ...
e.next()
})
onModelAfterUpdate -> onRecordAfterUpdateSuccess
onModelAfterUpdate((e) => {
// e.model ...
})
// note: onModelAfterUpdateSuccess is available if you are targeting non-Record/Collection models
onRecordAfterUpdateSuccess((e) => {
// e.record ...
e.next()
})
onModelBeforeDelete -> onRecordDelete
onModelBeforeDelete((e) => {
// e.model ...
})
// note: onModelDelete is available if you are targeting non-Record/Collection models
onRecordDelete((e) => {
// e.record ...
e.next()
})
onModelAfterDelete -> onRecordAfterDeleteSuccess
onModelAfterDelete((e) => {
// e.model ...
})
// note: onModelAfterDeleteSuccess is available if you are targeting non-Record/Collection models
onRecordAfterDeleteSuccess((e) => {
// e.record ...
e.next()
})
onMailerBeforeAdminResetPasswordSend -> onMailerRecordPasswordResetSend
onMailerBeforeAdminResetPasswordSend((e) => {
// ...
})
onMailerRecordPasswordResetSend((e) => {
// e.admin -> e.record ...
e.next()
}, "_superusers")
onMailerAfterAdminResetPasswordSend -> onMailerRecordPasswordResetSend
onMailerAfterAdminResetPasswordSend((e) => {
// ...
})
onMailerRecordPasswordResetSend((e) => {
e.next()
// e.admin -> e.record ...
}, "_superusers")
onMailerBeforeRecordResetPasswordSend -> onMailerRecordPasswordResetSend
onMailerBeforeRecordResetPasswordSend((e) => {
// ...
})
onMailerRecordPasswordResetSend((e) => {
// ...
e.next()
})
onMailerAfterRecordResetPasswordSend -> onMailerRecordPasswordResetSend
onMailerAfterRecordResetPasswordSend((e) => {
// ...
})
onMailerRecordPasswordResetSend((e) => {
e.next()
// ...
})
onMailerBeforeRecordVerificationSend -> onMailerRecordVerificationSend
onMailerBeforeRecordVerificationSend((e) => {
// ...
})
onMailerRecordVerificationSend((e) => {
// ...
e.next()
})
onMailerAfterRecordVerificationSend -> onMailerRecordVerificationSend
onMailerAfterRecordVerificationSend((e) => {
// ...
})
onMailerRecordVerificationSend((e) => {
e.next()
// ...
})
onMailerBeforeRecordChangeEmailSend -> onMailerRecordEmailChangeSend
onMailerBeforeRecordChangeEmailSend((e) => {
// ...
})
onMailerRecordEmailChangeSend((e) => {
// ...
e.next()
})
onMailerAfterRecordChangeEmailSend -> onMailerRecordEmailChangeSend
onMailerAfterRecordChangeEmailSend((e) => {
// ...
})
onMailerRecordEmailChangeSend((e) => {
e.next()
// ...
})
onRecordsListRequest
Side-note: if you are using this hook simply for adding/removing record fields you may want to check
onRecordEnrich
instead
onRecordsListRequest((e) => {
// ...
})
onRecordsListRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordViewRequest
Side-note: if you are using this hook simply for adding/removing record fields you may want to check
onRecordEnrich
instead
onRecordViewRequest((e) => {
// ...
})
onRecordViewRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordBeforeCreateRequest -> onRecordCreateRequest
With v0.23+ the hook fires before the record validations allowing you to adjust "Nonempty" fields before checking the constraint.
onRecordBeforeCreateRequest((e) => {
// ...
})
onRecordCreateRequest((e) => {
// uploaded files are simple values as part of the e.record;
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterCreateRequest -> onRecordCreateRequest
onRecordAfterCreateRequest((e) => {
// ...
})
onRecordCreateRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeUpdateRequest -> onRecordUpdateRequest
With v0.23+ the hook fires before the record validations allowing you to adjust "Nonempty" fields before checking the constraint.
onRecordBeforeUpdateRequest((e) => {
// ...
})
onRecordUpdateRequest((e) => {
// uploaded files are simple values as part of the e.record;
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterUpdateRequest -> onRecordUpdateRequest
onRecordAfterUpdateRequest((e) => {
// ...
})
onRecordUpdateRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeDeleteRequest -> onRecordDeleteRequest
onRecordBeforeDeleteRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterDeleteRequest -> onRecordDeleteRequest
onRecordAfterDeleteRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordAuthRequest
onRecordAuthRequest((e) => {
// ...
})
onRecordAuthRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordBeforeAuthWithPasswordRequest -> onRecordAuthWithPasswordRequest
onRecordBeforeAuthWithPasswordRequest((e) => {
// ...
})
onRecordAuthWithPasswordRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterAuthWithPasswordRequest -> onRecordAuthWithPasswordRequest
onRecordAfterAuthWithPasswordRequest((e) => {
// ...
})
onRecordAuthWithPasswordRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeAuthWithOAuth2Request -> onRecordAuthWithOAuth2Request
onRecordBeforeAuthWithOAuth2Request((e) => {
// ...
})
onRecordAuthWithOAuth2Request((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterAuthWithOAuth2Request -> onRecordAuthWithOAuth2Request
onRecordAfterAuthWithOAuth2Request((e) => {
// ...
})
onRecordAuthWithOAuth2Request((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeAuthRefreshRequest -> onRecordAuthRefreshRequest
onRecordBeforeAuthRefreshRequest((e) => {
// ...
})
onRecordAuthRefreshRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterAuthRefreshRequest -> onRecordAuthRefreshRequest
onRecordAfterAuthRefreshRequest((e) => {
// ...
})
onRecordAuthRefreshRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordListExternalAuthsRequest -> onRecordsListRequest
External auths are converted to system_externalAuths
collection records. onRecordListExternalAuthsRequest((e) => {
// ...
})
onRecordsListRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_externalAuths")
onRecordBeforeUnlinkExternalAuthRequest -> onRecordDeleteRequest
External auths are converted to system_externalAuths
collection records. onRecordBeforeUnlinkExternalAuthRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_externalAuths")
onRecordAfterUnlinkExternalAuthRequest -> onRecordDeleteRequest
External auths are converted to system_externalAuths
collection records. onRecordAfterUnlinkExternalAuthRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_externalAuths")
onRecordBeforeRequestPasswordResetRequest -> onRecordRequestPasswordResetRequest
onRecordBeforeRequestPasswordResetRequest((e) => {
// ...
})
onRecordRequestPasswordResetRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterRequestPasswordResetRequest -> onRecordRequestPasswordResetRequest
onRecordAfterRequestPasswordResetRequest((e) => {
// ...
})
onRecordRequestPasswordResetRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeConfirmPasswordResetRequest -> onRecordConfirmPasswordResetRequest
onRecordBeforeConfirmPasswordResetRequest((e) => {
// ...
})
onRecordConfirmPasswordResetRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterConfirmPasswordResetRequest -> onRecordConfirmPasswordResetRequest
onRecordAfterConfirmPasswordResetRequest((e) => {
// ...
})
onRecordConfirmPasswordResetRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeRequestVerificationRequest -> onRecordRequestVerificationRequest
onRecordBeforeRequestVerificationRequest((e) => {
// ...
})
onRecordRequestVerificationRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterRequestVerificationRequest -> onRecordRequestVerificationRequest
onRecordAfterRequestVerificationRequest((e) => {
// ...
})
onRecordRequestVerificationRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeConfirmVerificationRequest -> onRecordConfirmVerificationRequest
onRecordBeforeConfirmVerificationRequest((e) => {
// ...
})
onRecordConfirmVerificationRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterConfirmVerificationRequest -> onRecordConfirmVerificationRequest
onRecordAfterConfirmVerificationRequest((e) => {
// ...
})
onRecordConfirmVerificationRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeRequestEmailChangeRequest -> onRecordRequestEmailChangeRequest
onRecordBeforeRequestEmailChangeRequest((e) => {
// ...
})
onRecordRequestEmailChangeRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterRequestEmailChangeRequest -> onRecordRequestEmailChangeRequest
onRecordAfterRequestEmailChangeRequest((e) => {
// ...
})
onRecordRequestEmailChangeRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRecordBeforeConfirmEmailChangeRequest -> onRecordConfirmEmailChangeRequest
onRecordBeforeConfirmEmailChangeRequest((e) => {
// ...
})
onRecordConfirmEmailChangeRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRecordAfterConfirmEmailChangeRequest -> onRecordConfirmEmailChangeRequest
onRecordAfterConfirmEmailChangeRequest((e) => {
// ...
})
onRecordConfirmEmailChangeRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRealtimeConnectRequest
onRealtimeConnectRequest((e) => {
// ...
})
onRealtimeConnectRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRealtimeDisconnectRequest -> onRealtimeConnectRequest
onRealtimeDisconnectRequest((e) => {
// ...
})
onRealtimeConnectRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onRealtimeBeforeMessageSend -> onRealtimeMessageSend
onRealtimeBeforeMessageSend((e) => {
// ...
})
onRealtimeMessageSend((e) => {
// ...
e.next()
})
onRealtimeAfterMessageSend -> onRealtimeMessageSend
onRealtimeAfterMessageSend((e) => {
// ...
})
onRealtimeMessageSend((e) => {
e.next()
// ...
})
onRealtimeBeforeSubscribeRequest -> onRealtimeSubscribeRequest
onRealtimeBeforeSubscribeRequest((e) => {
// ...
})
onRealtimeSubscribeRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onRealtimeAfterSubscribeRequest -> onRealtimeSubscribeRequest
onRealtimeAfterSubscribeRequest((e) => {
// ...
})
onRealtimeSubscribeRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onFileDownloadRequest
onFileDownloadRequest((e) => {
// ...
})
onFileDownloadRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onFileBeforeTokenRequest -> onFileTokenRequest
onFileBeforeTokenRequest((e) => {
// ...
})
onFileTokenRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onFileAfterTokenRequest -> onFileTokenRequest
onFileAfterTokenRequest((e) => {
// ...
})
onFileTokenRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onCollectionsListRequest
onCollectionsListRequest((e) => {
// ...
})
onCollectionsListRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onCollectionViewRequest
onCollectionViewRequest((e) => {
// ...
})
onCollectionViewRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onCollectionBeforeCreateRequest -> onCollectionCreateRequest
With v0.23+ the hook fires before the collection validations.
onCollectionBeforeCreateRequest((e) => {
// ...
})
onCollectionCreateRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onCollectionAfterCreateRequest -> onCollectionCreateRequest
onCollectionAfterCreateRequest((e) => {
// ...
})
onCollectionCreateRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onCollectionBeforeUpdateRequest -> onCollectionUpdateRequest
With v0.23+ the hook fires before the collection validations allowing you to adjust "Nonempty" fields before checking the constraint.
onCollectionBeforeUpdateRequest((e) => {
// ...
})
onCollectionUpdateRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onCollectionAfterUpdateRequest -> onCollectionUpdateRequest
onCollectionAfterUpdateRequest((e) => {
// ...
})
onCollectionUpdateRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onCollectionBeforeDeleteRequest -> onCollectionDeleteRequest
onCollectionBeforeDeleteRequest((e) => {
// ...
})
onCollectionDeleteRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onCollectionAfterDeleteRequest -> onCollectionDeleteRequest
onCollectionAfterDeleteRequest((e) => {
// ...
})
onCollectionDeleteRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onCollectionsBeforeImportRequest -> onCollectionsImportRequest
onCollectionBeforeImportRequest((e) => {
// ...
})
onCollectionsImportRequest((e) => {
// "e.collections" is replaced with "e.collectionsData" which is just a map...
e.next()
})
onCollectionsAfterImportRequest -> onCollectionsImportRequest
onCollectionsAfterImportRequest((e) => {
// ...
})
onCollectionsImportRequest((e) => {
e.next()
// "e.collections" is replaced with "e.collectionsData" which is just a map...
})
onSettingsListRequest
onSettingsListRequest((e) => {
// ...
})
onSettingsListRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onSettingsBeforeUpdateRequest -> onSettingsUpdateRequest
With v0.23+ the hook fires before the settings validations.
onSettingsBeforeUpdateRequest((e) => {
// ...
})
onSettingsUpdateRequest((e) => {
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
})
onSettingsAfterUpdateRequest -> onSettingsUpdateRequest
onSettingsAfterUpdateRequest((e) => {
// ...
})
onSettingsUpdateRequest((e) => {
e.next()
// "e.httpContext" is no longer available because "e" is the request event itself ...
})
onAdminsListRequest -> onRecordsListRequest
onAdminsListRequest((e) => {
// ...
})
onRecordsListRequest((e) => {
// e.admins -> e.records
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminViewRequest -> onRecordViewRequest
onAdminViewRequest((e) => {
// ...
})
onRecordViewRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminBeforeCreateRequest -> onRecordCreateRequest
onAdminBeforeCreateRequest((e) => {
// ...
})
onRecordCreateRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterCreateRequest -> onRecordCreateRequest
onAdminAfterCreateRequest((e) => {
// ...
})
onRecordCreateRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminBeforeUpdateRequest -> onRecordUpdateRequest
onAdminBeforeUpdateRequest((e) => {
// ...
})
onRecordUpdateRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterUpdateRequest -> onRecordUpdateRequest
onAdminAfterUpdateRequest((e) => {
// ...
})
onRecordUpdateRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminBeforeDeleteRequest -> onRecordDeleteRequest
onAdminBeforeDeleteRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterDeleteRequest -> onRecordDeleteRequest
onAdminAfterDeleteRequest((e) => {
// ...
})
onRecordDeleteRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminAuthRequest -> onRecordAuthRequest
onAdminAuthRequest((e) => {
// ...
})
onRecordAuthRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminBeforeAuthWithPasswordRequest -> onRecordAuthWithPasswordRequest
onAdminBeforeAuthWithPasswordRequest((e) => {
// ...
})
onRecordAuthWithPasswordRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterAuthWithPasswordRequest -> onRecordAuthWithPasswordRequest
onAdminAfterAuthWithPasswordRequest((e) => {
// ...
})
onRecordAuthWithPasswordRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminBeforeAuthRefreshRequest -> onRecordAuthRefreshRequest
onAdminBeforeAuthRefreshRequest((e) => {
// ...
})
onRecordAuthRefreshRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterAuthRefreshRequest -> onRecordAuthRefreshRequest
onAdminAfterAuthRefreshRequest((e) => {
// ...
})
onRecordAuthRefreshRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminBeforeRequestPasswordResetRequest -> onRecordRequestPasswordResetRequest
onAdminBeforeRequestPasswordResetRequest((e) => {
// ...
})
onRecordRequestPasswordResetRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterRequestPasswordResetRequest -> onRecordRequestPasswordResetRequest
onAdminAfterRequestPasswordResetRequest((e) => {
// ...
})
onRecordRequestPasswordResetRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
onAdminBeforeConfirmPasswordResetRequest -> onRecordConfirmPasswordResetRequest
onAdminBeforeConfirmPasswordResetRequest((e) => {
// ...
})
onRecordConfirmPasswordResetRequest((e) => {
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
e.next()
}, "_superusers")
onAdminAfterConfirmPasswordResetRequest -> onRecordConfirmPasswordResetRequest
onAdminAfterConfirmPasswordResetRequest((e) => {
// ...
})
onRecordConfirmPasswordResetRequest((e) => {
e.next()
// e.admin -> e.record
// "e.httpContext" is no longer available because "e" is the request event itself ...
}, "_superusers")
Other minor changes
$http.send()
$http.send()
no longer sends a default Content-Type: application/json
header with
the request.
const res = $http.send({
url: "https://example.com/data.json",
})
const res = $http.send({
url: "https://example.com/data.json",
headers: { "content-type": "application/json"}
})