diff --git a/README.md b/README.md
index 9deb16cd4f..7827d4e48a 100644
--- a/README.md
+++ b/README.md
@@ -126,13 +126,6 @@ You can learn more about the Budibase API at the following places:
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
-
-
-
-
-
-
-
## 🏁 Get started
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts
index e64c116663..c331d791a6 100644
--- a/packages/backend-core/src/cache/writethrough.ts
+++ b/packages/backend-core/src/cache/writethrough.ts
@@ -119,8 +119,8 @@ export class Writethrough {
this.writeRateMs = writeRateMs
}
- async put(doc: any) {
- return put(this.db, doc, this.writeRateMs)
+ async put(doc: any, writeRateMs: number = this.writeRateMs) {
+ return put(this.db, doc, writeRateMs)
}
async get(id: string) {
diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts
index b05cf79c8c..0d33031de5 100644
--- a/packages/backend-core/src/security/roles.ts
+++ b/packages/backend-core/src/security/roles.ts
@@ -122,7 +122,9 @@ export async function roleToNumber(id?: string) {
if (isBuiltin(id)) {
return builtinRoleToNumber(id)
}
- const hierarchy = (await getUserRoleHierarchy(id)) as RoleDoc[]
+ const hierarchy = (await getUserRoleHierarchy(id, {
+ defaultPublic: true,
+ })) as RoleDoc[]
for (let role of hierarchy) {
if (isBuiltin(role?.inherits)) {
return builtinRoleToNumber(role.inherits) + 1
@@ -192,12 +194,15 @@ export async function getRole(
/**
* Simple function to get all the roles based on the top level user role ID.
*/
-async function getAllUserRoles(userRoleId?: string): Promise {
+async function getAllUserRoles(
+ userRoleId?: string,
+ opts?: { defaultPublic?: boolean }
+): Promise {
// admins have access to all roles
if (userRoleId === BUILTIN_IDS.ADMIN) {
return getAllRoles()
}
- let currentRole = await getRole(userRoleId)
+ let currentRole = await getRole(userRoleId, opts)
let roles = currentRole ? [currentRole] : []
let roleIds = [userRoleId]
// get all the inherited roles
@@ -226,12 +231,16 @@ export async function getUserRoleIdHierarchy(
* Returns an ordered array of the user's inherited role IDs, this can be used
* to determine if a user can access something that requires a specific role.
* @param userRoleId The user's role ID, this can be found in their access token.
+ * @param opts optional - if want to default to public use this.
* @returns returns an ordered array of the roles, with the first being their
* highest level of access and the last being the lowest level.
*/
-export async function getUserRoleHierarchy(userRoleId?: string) {
+export async function getUserRoleHierarchy(
+ userRoleId?: string,
+ opts?: { defaultPublic?: boolean }
+) {
// special case, if they don't have a role then they are a public user
- return getAllUserRoles(userRoleId)
+ return getAllUserRoles(userRoleId, opts)
}
// this function checks that the provided permissions are in an array format
diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts
index a2539e836e..daa09bee6f 100644
--- a/packages/backend-core/src/users/db.ts
+++ b/packages/backend-core/src/users/db.ts
@@ -25,12 +25,17 @@ import {
import {
getAccountHolderFromUserIds,
isAdmin,
+ isCreator,
validateUniqueUser,
} from "./utils"
import { searchExistingEmails } from "./lookup"
import { hash } from "../utils"
-type QuotaUpdateFn = (change: number, cb?: () => Promise) => Promise
+type QuotaUpdateFn = (
+ change: number,
+ creatorsChange: number,
+ cb?: () => Promise
+) => Promise
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise
type FeatureFn = () => Promise
type GroupGetFn = (ids: string[]) => Promise
@@ -245,7 +250,8 @@ export class UserDB {
}
const change = dbUser ? 0 : 1 // no change if there is existing user
- return UserDB.quotas.addUsers(change, async () => {
+ const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
+ return UserDB.quotas.addUsers(change, creatorsChange, async () => {
await validateUniqueUser(email, tenantId)
let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
@@ -307,6 +313,7 @@ export class UserDB {
let usersToSave: any[] = []
let newUsers: any[] = []
+ let newCreators: any[] = []
const emails = newUsersRequested.map((user: User) => user.email)
const existingEmails = await searchExistingEmails(emails)
@@ -327,59 +334,66 @@ export class UserDB {
}
newUser.userGroups = groups
newUsers.push(newUser)
+ if (isCreator(newUser)) {
+ newCreators.push(newUser)
+ }
}
const account = await accountSdk.getAccountByTenantId(tenantId)
- return UserDB.quotas.addUsers(newUsers.length, async () => {
- // create the promises array that will be called by bulkDocs
- newUsers.forEach((user: any) => {
- usersToSave.push(
- UserDB.buildUser(
- user,
- {
- hashPassword: true,
- requirePassword: user.requirePassword,
- },
- tenantId,
- undefined, // no dbUser
- account
+ return UserDB.quotas.addUsers(
+ newUsers.length,
+ newCreators.length,
+ async () => {
+ // create the promises array that will be called by bulkDocs
+ newUsers.forEach((user: any) => {
+ usersToSave.push(
+ UserDB.buildUser(
+ user,
+ {
+ hashPassword: true,
+ requirePassword: user.requirePassword,
+ },
+ tenantId,
+ undefined, // no dbUser
+ account
+ )
)
- )
- })
+ })
- const usersToBulkSave = await Promise.all(usersToSave)
- await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
+ const usersToBulkSave = await Promise.all(usersToSave)
+ await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
- // Post-processing of bulk added users, e.g. events and cache operations
- for (const user of usersToBulkSave) {
- // TODO: Refactor to bulk insert users into the info db
- // instead of relying on looping tenant creation
- await platform.users.addUser(tenantId, user._id, user.email)
- await eventHelpers.handleSaveEvents(user, undefined)
- }
+ // Post-processing of bulk added users, e.g. events and cache operations
+ for (const user of usersToBulkSave) {
+ // TODO: Refactor to bulk insert users into the info db
+ // instead of relying on looping tenant creation
+ await platform.users.addUser(tenantId, user._id, user.email)
+ await eventHelpers.handleSaveEvents(user, undefined)
+ }
+
+ const saved = usersToBulkSave.map(user => {
+ return {
+ _id: user._id,
+ email: user.email,
+ }
+ })
+
+ // now update the groups
+ if (Array.isArray(saved) && groups) {
+ const groupPromises = []
+ const createdUserIds = saved.map(user => user._id)
+ for (let groupId of groups) {
+ groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
+ }
+ await Promise.all(groupPromises)
+ }
- const saved = usersToBulkSave.map(user => {
return {
- _id: user._id,
- email: user.email,
+ successful: saved,
+ unsuccessful,
}
- })
-
- // now update the groups
- if (Array.isArray(saved) && groups) {
- const groupPromises = []
- const createdUserIds = saved.map(user => user._id)
- for (let groupId of groups) {
- groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
- }
- await Promise.all(groupPromises)
}
-
- return {
- successful: saved,
- unsuccessful,
- }
- })
+ )
}
static async bulkDelete(userIds: string[]): Promise {
@@ -419,11 +433,12 @@ export class UserDB {
_deleted: true,
}))
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
+ const creatorsToDelete = usersToDelete.filter(isCreator)
- await UserDB.quotas.removeUsers(toDelete.length)
for (let user of usersToDelete) {
await bulkDeleteProcessing(user)
}
+ await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
// Build Response
// index users by id
@@ -472,7 +487,8 @@ export class UserDB {
await db.remove(userId, dbUser._rev)
- await UserDB.quotas.removeUsers(1)
+ const creatorsToDelete = isCreator(dbUser) ? 1 : 0
+ await UserDB.quotas.removeUsers(1, creatorsToDelete)
await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId)
await sessions.invalidateSessions(userId, { reason: "deletion" })
diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts
index 6237c23972..bad108ab84 100644
--- a/packages/backend-core/src/users/users.ts
+++ b/packages/backend-core/src/users/users.ts
@@ -14,11 +14,11 @@ import {
} from "../db"
import {
BulkDocsResponse,
- ContextUser,
SearchQuery,
SearchQueryOperators,
SearchUsersRequest,
User,
+ ContextUser,
DatabaseQueryOpts,
} from "@budibase/types"
import { getGlobalDB } from "../context"
diff --git a/packages/backend-core/tests/core/users/users.spec.js b/packages/backend-core/tests/core/users/users.spec.js
new file mode 100644
index 0000000000..ae7109344a
--- /dev/null
+++ b/packages/backend-core/tests/core/users/users.spec.js
@@ -0,0 +1,54 @@
+const _ = require('lodash/fp')
+const {structures} = require("../../../tests")
+
+jest.mock("../../../src/context")
+jest.mock("../../../src/db")
+
+const context = require("../../../src/context")
+const db = require("../../../src/db")
+
+const {getCreatorCount} = require('../../../src/users/users')
+
+describe("Users", () => {
+
+ let getGlobalDBMock
+ let getGlobalUserParamsMock
+ let paginationMock
+
+ beforeEach(() => {
+ jest.resetAllMocks()
+
+ getGlobalDBMock = jest.spyOn(context, "getGlobalDB")
+ getGlobalUserParamsMock = jest.spyOn(db, "getGlobalUserParams")
+ paginationMock = jest.spyOn(db, "pagination")
+ })
+
+ it("Retrieves the number of creators", async () => {
+ const getUsers = (offset, limit, creators = false) => {
+ const range = _.range(offset, limit)
+ const opts = creators ? {builder: {global: true}} : undefined
+ return range.map(() => structures.users.user(opts))
+ }
+ const page1Data = getUsers(0, 8)
+ const page2Data = getUsers(8, 12, true)
+ getGlobalDBMock.mockImplementation(() => ({
+ name : "fake-db",
+ allDocs: () => ({
+ rows: [...page1Data, ...page2Data]
+ })
+ }))
+ paginationMock.mockImplementationOnce(() => ({
+ data: page1Data,
+ hasNextPage: true,
+ nextPage: "1"
+ }))
+ paginationMock.mockImplementation(() => ({
+ data: page2Data,
+ hasNextPage: false,
+ nextPage: undefined
+ }))
+ const creatorsCount = await getCreatorCount()
+ expect(creatorsCount).toBe(4)
+ expect(paginationMock).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts
index 0e34f2e9bb..bb452f9ad5 100644
--- a/packages/backend-core/tests/core/utilities/structures/licenses.ts
+++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts
@@ -123,6 +123,10 @@ export function customer(): Customer {
export function subscription(): Subscription {
return {
amount: 10000,
+ amounts: {
+ user: 10000,
+ creator: 0,
+ },
cancelAt: undefined,
currency: "usd",
currentPeriodEnd: 0,
@@ -131,6 +135,10 @@ export function subscription(): Subscription {
duration: PriceDuration.MONTHLY,
pastDueAt: undefined,
quantity: 0,
+ quantities: {
+ user: 0,
+ creator: 0,
+ },
status: "active",
}
}
diff --git a/packages/pro b/packages/pro
index 5ed0ee2aca..3820c0c93a 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e
+Subproject commit 3820c0c93a3e448e10a60a9feb5396844b537ca8
diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts
index c29cb65eac..516bfd20c6 100644
--- a/packages/server/src/api/routes/row.ts
+++ b/packages/server/src/api/routes/row.ts
@@ -11,128 +11,24 @@ const { PermissionType, PermissionLevel } = permissions
const router: Router = new Router()
router
- /**
- * @api {get} /api/:sourceId/:rowId/enrich Get an enriched row
- * @apiName Get an enriched row
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This API is only useful when dealing with rows that have relationships.
- * Normally when a row is a returned from the API relationships will only have the structure
- * `{ primaryDisplay: "name", _id: ... }` but this call will return the full related rows
- * for each relationship instead.
- *
- * @apiParam {string} rowId The ID of the row which is to be retrieved and enriched.
- *
- * @apiSuccess {object} row The response body will be the enriched row.
- */
.get(
"/api/:sourceId/:rowId/enrich",
paramSubResource("sourceId", "rowId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetchEnrichedRow
)
- /**
- * @api {get} /api/:sourceId/rows Get all rows in a table
- * @apiName Get all rows in a table
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This is a deprecated endpoint that should not be used anymore, instead use the search endpoint.
- * This endpoint gets all of the rows within the specified table - it is not heavily used
- * due to its lack of support for pagination. With SQL tables this will retrieve up to a limit and then
- * will simply stop.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve all rows within.
- *
- * @apiSuccess {object[]} rows The response body will be an array of all rows found.
- */
.get(
"/api/:sourceId/rows",
paramResource("sourceId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetch
)
- /**
- * @api {get} /api/:sourceId/rows/:rowId Retrieve a single row
- * @apiName Retrieve a single row
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This endpoint retrieves only the specified row. If you wish to retrieve
- * a row by anything other than its _id field, use the search endpoint.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve a row from.
- * @apiParam {string} rowId The ID of the row to retrieve.
- *
- * @apiSuccess {object} body The response body will be the row that was found.
- */
.get(
"/api/:sourceId/rows/:rowId",
paramSubResource("sourceId", "rowId"),
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.find
)
- /**
- * @api {post} /api/:sourceId/search Search for rows in a table
- * @apiName Search for rows in a table
- * @apiGroup rows
- * @apiPermission table read access
- * @apiDescription This is the primary method of accessing rows in Budibase, the data provider
- * and data UI in the builder are built atop this. All filtering, sorting and pagination is
- * handled through this, for internal and external (datasource plus, e.g. SQL) tables.
- *
- * @apiParam {string} sourceId The ID of the table to retrieve rows from.
- *
- * @apiParam (Body) {boolean} [paginate] If pagination is required then this should be set to true,
- * defaults to false.
- * @apiParam (Body) {object} [query] This contains a set of filters which should be applied, if none
- * specified then the request will be unfiltered. An example with all of the possible query
- * options has been supplied below.
- * @apiParam (Body) {number} [limit] This sets a limit for the number of rows that will be returned,
- * this will be implemented at the database level if supported for performance reasons. This
- * is useful when paginating to set exactly how many rows per page.
- * @apiParam (Body) {string} [bookmark] If pagination is enabled then a bookmark will be returned
- * with each successful search request, this should be supplied back to get the next page.
- * @apiParam (Body) {object} [sort] If sort is desired this should contain the name of the column to
- * sort on.
- * @apiParam (Body) {string} [sortOrder] If sort is enabled then this can be either "descending" or
- * "ascending" as required.
- * @apiParam (Body) {string} [sortType] If sort is enabled then you must specify the type of search
- * being used, either "string" or "number". This is only used for internal tables.
- *
- * @apiParamExample {json} Example:
- * {
- * "tableId": "ta_70260ff0b85c467ca74364aefc46f26d",
- * "query": {
- * "string": {},
- * "fuzzy": {},
- * "range": {
- * "columnName": {
- * "high": 20,
- * "low": 10,
- * }
- * },
- * "equal": {
- * "columnName": "someValue"
- * },
- * "notEqual": {},
- * "empty": {},
- * "notEmpty": {},
- * "oneOf": {
- * "columnName": ["value"]
- * }
- * },
- * "limit": 10,
- * "sort": "name",
- * "sortOrder": "descending",
- * "sortType": "string",
- * "paginate": true
- * }
- *
- * @apiSuccess {object[]} rows An array of rows that was found based on the supplied parameters.
- * @apiSuccess {boolean} hasNextPage If pagination was enabled then this specifies whether or
- * not there is another page after this request.
- * @apiSuccess {string} bookmark The bookmark to be sent with the next request to get the next
- * page.
- */
.post(
"/api/:sourceId/search",
internalSearchValidator(),
@@ -148,30 +44,6 @@ router
authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.search
)
- /**
- * @api {post} /api/:sourceId/rows Creates a new row
- * @apiName Creates a new row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This API will create a new row based on the supplied body. If the
- * body includes an "_id" field then it will update an existing row if the field
- * links to one. Please note that "_id", "_rev" and "tableId" are fields that are
- * already used by Budibase tables and cannot be used for columns.
- *
- * @apiParam {string} sourceId The ID of the table to save a row to.
- *
- * @apiParam (Body) {string} [_id] If the row exists already then an ID for the row must be provided.
- * @apiParam (Body) {string} [_rev] If working with an existing row for an internal table its revision
- * must also be provided.
- * @apiParam (Body) {string} tableId The ID of the table should also be specified in the row body itself.
- * @apiParam (Body) {any} [any] Any field supplied in the body will be assessed to see if it matches
- * a column in the specified table. All other fields will be dropped and not stored.
- *
- * @apiSuccess {string} _id The ID of the row that was just saved, if it was just created this
- * is the rows new ID.
- * @apiSuccess {string} [_rev] If saving to an internal table a revision will also be returned.
- * @apiSuccess {object} body The contents of the row that was saved will be returned as well.
- */
.post(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -179,14 +51,6 @@ router
trimViewRowInfo,
rowController.save
)
- /**
- * @api {patch} /api/:sourceId/rows Updates a row
- * @apiName Update a row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This endpoint is identical to the row creation endpoint but instead it will
- * error if an _id isn't provided, it will only function for existing rows.
- */
.patch(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -194,52 +58,12 @@ router
trimViewRowInfo,
rowController.patch
)
- /**
- * @api {post} /api/:sourceId/rows/validate Validate inputs for a row
- * @apiName Validate inputs for a row
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription When attempting to save a row you may want to check if the row is valid
- * given the table schema, this will iterate through all the constraints on the table and
- * check if the request body is valid.
- *
- * @apiParam {string} sourceId The ID of the table the row is to be validated for.
- *
- * @apiParam (Body) {any} [any] Any fields provided in the request body will be tested
- * against the table schema and constraints.
- *
- * @apiSuccess {boolean} valid If inputs provided are acceptable within the table schema this
- * will be true, if it is not then then errors property will be populated.
- * @apiSuccess {object} [errors] A key value map of information about fields on the input
- * which do not match the table schema. The key name will be the column names that have breached
- * the schema.
- */
.post(
"/api/:sourceId/rows/validate",
paramResource("sourceId"),
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.validate
)
- /**
- * @api {delete} /api/:sourceId/rows Delete rows
- * @apiName Delete rows
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This endpoint can delete a single row, or delete them in a bulk
- * fashion.
- *
- * @apiParam {string} sourceId The ID of the table the row is to be deleted from.
- *
- * @apiParam (Body) {object[]} [rows] If bulk deletion is desired then provide the rows in this
- * key of the request body that are to be deleted.
- * @apiParam (Body) {string} [_id] If deleting a single row then provide its ID in this field.
- * @apiParam (Body) {string} [_rev] If deleting a single row from an internal table then provide its
- * revision here.
- *
- * @apiSuccess {object[]|object} body If deleting bulk then the response body will be an array
- * of the deleted rows, if deleting a single row then the body will contain a "row" property which
- * is the deleted row.
- */
.delete(
"/api/:sourceId/rows",
paramResource("sourceId"),
@@ -247,20 +71,6 @@ router
trimViewRowInfo,
rowController.destroy
)
-
- /**
- * @api {post} /api/:sourceId/rows/exportRows Export Rows
- * @apiName Export rows
- * @apiGroup rows
- * @apiPermission table write access
- * @apiDescription This API can export a number of provided rows
- *
- * @apiParam {string} sourceId The ID of the table the row is to be deleted from.
- *
- * @apiParam (Body) {object[]} [rows] The row IDs which are to be exported
- *
- * @apiSuccess {object[]|object}
- */
.post(
"/api/:sourceId/rows/exportRows",
paramResource("sourceId"),
diff --git a/packages/server/src/api/routes/table.ts b/packages/server/src/api/routes/table.ts
index 7ffa5acb3e..0172d9844d 100644
--- a/packages/server/src/api/routes/table.ts
+++ b/packages/server/src/api/routes/table.ts
@@ -9,99 +9,13 @@ const { BUILDER, PermissionLevel, PermissionType } = permissions
const router: Router = new Router()
router
- /**
- * @api {get} /api/tables Fetch all tables
- * @apiName Fetch all tables
- * @apiGroup tables
- * @apiPermission table read access
- * @apiDescription This endpoint retrieves all of the tables which have been created in
- * an app. This includes all of the external and internal tables; to tell the difference
- * between these look for the "type" property on each table, either being "internal" or "external".
- *
- * @apiSuccess {object[]} body The response body will be the list of tables that was found - as
- * this does not take any parameters the only error scenario is no access.
- */
.get("/api/tables", authorized(BUILDER), tableController.fetch)
- /**
- * @api {get} /api/tables/:id Fetch a single table
- * @apiName Fetch a single table
- * @apiGroup tables
- * @apiPermission table read access
- * @apiDescription Retrieves a single table this could be be internal or external based on
- * the provided table ID.
- *
- * @apiParam {string} id The ID of the table which is to be retrieved.
- *
- * @apiSuccess {object[]} body The response body will be the table that was found.
- */
.get(
"/api/tables/:tableId",
paramResource("tableId"),
authorized(PermissionType.TABLE, PermissionLevel.READ, { schema: true }),
tableController.find
)
- /**
- * @api {post} /api/tables Save a table
- * @apiName Save a table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription Create or update a table with this endpoint, this will function for both internal
- * external tables.
- *
- * @apiParam (Body) {string} [_id] If updating an existing table then the ID of the table must be specified.
- * @apiParam (Body) {string} [_rev] If updating an existing internal table then the revision must also be specified.
- * @apiParam (Body) {string} type] This should either be "internal" or "external" depending on the table type -
- * this will default to internal.
- * @apiParam (Body) {string} [sourceId] If creating an external table then this should be set to the datasource ID. If
- * building an internal table this does not need to be set, although it will be returned as "bb_internal".
- * @apiParam (Body) {string} name The name of the table, this will be used in the UI. To rename the table simply
- * supply the table structure to this endpoint with the name changed.
- * @apiParam (Body) {object} schema A key value object which has all of the columns in the table as the keys in this
- * object. For each column a "type" and "constraints" must be specified, with some types requiring further information.
- * More information about the schema structure can be found in the Typescript definitions.
- * @apiParam (Body) {string} [primaryDisplay] The name of the column which should be used when displaying rows
- * from this table as relationships.
- * @apiParam (Body) {object[]} [indexes] Specifies the search indexes - this is deprecated behaviour with the introduction
- * of lucene indexes. This functionality is only available for internal tables.
- * @apiParam (Body) {object} [_rename] If a column is to be renamed then the "old" column name should be set in this
- * structure, and the "updated", new column name should also be supplied. The schema should also be updated, this field
- * lets the server know that a field hasn't just been deleted, that the data has moved to a new name, this will fix
- * the rows in the table. This functionality is only available for internal tables.
- * @apiParam (Body) {object[]} [rows] When creating a table using a compatible data source, an array of objects to be imported into the new table can be provided.
- *
- * @apiParamExample {json} Example:
- * {
- * "_id": "ta_05541307fa0f4044abee071ca2a82119",
- * "_rev": "10-0fbe4e78f69b255d79f1017e2eeef807",
- * "type": "internal",
- * "views": {},
- * "name": "tableName",
- * "schema": {
- * "column": {
- * "type": "string",
- * "constraints": {
- * "type": "string",
- * "length": {
- * "maximum": null
- * },
- * "presence": false
- * },
- * "name": "column"
- * },
- * },
- * "primaryDisplay": "column",
- * "indexes": [],
- * "sourceId": "bb_internal",
- * "_rename": {
- * "old": "columnName",
- * "updated": "newColumnName",
- * },
- * "rows": []
- * }
- *
- * @apiSuccess {object} table The response body will contain the table structure after being cleaned up and
- * saved to the database.
- */
.post(
"/api/tables",
// allows control over updating a table
@@ -125,41 +39,12 @@ router
authorized(BUILDER),
tableController.validateExistingTableImport
)
- /**
- * @api {post} /api/tables/:tableId/:revId Delete a table
- * @apiName Delete a table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription This endpoint will delete a table and all of its associated data, for this reason it is
- * quite dangerous - it will work for internal and external tables.
- *
- * @apiParam {string} tableId The ID of the table which is to be deleted.
- * @apiParam {string} [revId] If deleting an internal table then the revision must also be supplied (_rev), for
- * external tables this can simply be set to anything, e.g. "external".
- *
- * @apiSuccess {string} message A message stating that the table was deleted successfully.
- */
.delete(
"/api/tables/:tableId/:revId",
paramResource("tableId"),
authorized(BUILDER),
tableController.destroy
)
- /**
- * @api {post} /api/tables/:tableId/:revId Import CSV to existing table
- * @apiName Import CSV to existing table
- * @apiGroup tables
- * @apiPermission builder
- * @apiDescription This endpoint will import data to existing tables, internal or external. It is used in combination
- * with the CSV validation endpoint. Take the output of the CSV validation endpoint and pass it to this endpoint to
- * import the data; please note this will only import fields that already exist on the table/match the type.
- *
- * @apiParam {string} tableId The ID of the table which the data should be imported to.
- *
- * @apiParam (Body) {object[]} rows An array of objects representing the rows to be imported, key-value pairs not matching the table schema will be ignored.
- *
- * @apiSuccess {string} message A message stating that the data was imported successfully.
- */
.post(
"/api/tables/:tableId/import",
paramResource("tableId"),
diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts
index 53aa4842c4..e3935bc7ee 100644
--- a/packages/types/src/sdk/featureFlag.ts
+++ b/packages/types/src/sdk/featureFlag.ts
@@ -1,5 +1,8 @@
export enum FeatureFlag {
LICENSING = "LICENSING",
+ // Feature IDs in Posthog
+ PER_CREATOR_PER_USER_PRICE = "18873",
+ PER_CREATOR_PER_USER_PRICE_ALERT = "18530",
}
export interface TenantFeatureFlags {
diff --git a/packages/types/src/sdk/licensing/billing.ts b/packages/types/src/sdk/licensing/billing.ts
index 35f366c811..bcbc7abd18 100644
--- a/packages/types/src/sdk/licensing/billing.ts
+++ b/packages/types/src/sdk/licensing/billing.ts
@@ -5,10 +5,17 @@ export interface Customer {
currency: string | null | undefined
}
+export interface SubscriptionItems {
+ user: number | undefined
+ creator: number | undefined
+}
+
export interface Subscription {
amount: number
+ amounts: SubscriptionItems | undefined
currency: string
quantity: number
+ quantities: SubscriptionItems | undefined
duration: PriceDuration
cancelAt: number | null | undefined
currentPeriodStart: number
diff --git a/packages/types/src/sdk/licensing/plan.ts b/packages/types/src/sdk/licensing/plan.ts
index 3e214a01ff..1604dfb8af 100644
--- a/packages/types/src/sdk/licensing/plan.ts
+++ b/packages/types/src/sdk/licensing/plan.ts
@@ -4,7 +4,9 @@ export enum PlanType {
PRO = "pro",
/** @deprecated */
TEAM = "team",
+ /** @deprecated */
PREMIUM = "premium",
+ PREMIUM_PLUS = "premium_plus",
BUSINESS = "business",
ENTERPRISE = "enterprise",
}
@@ -26,10 +28,12 @@ export interface AvailablePrice {
currency: string
duration: PriceDuration
priceId: string
+ type?: string
}
export enum PlanModel {
PER_USER = "perUser",
+ PER_CREATOR_PER_USER = "per_creator_per_user",
DAY_PASS = "dayPass",
}