Initial commit: Korrith website, assessment API, PocketBase schema, Docker/Coolify deployment

This commit is contained in:
Korrith
2026-09-17 20:47:51 +00:00
commit c17f11f591
248 changed files with 39721 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
pb_data
pb_backups
pb_snapshots
pocketbase
.pocketbase-sha256
database-types.d.ts
+1
View File
@@ -0,0 +1 @@
66396ed5efec837aa8d2addc009894968a5f4fb7cb2b0ea57db6105cb91b095d
+1
View File
@@ -0,0 +1 @@
0.40.4
+27
View File
@@ -0,0 +1,27 @@
# PocketBase 0.40.4 — the schema in pb_migrations is applied automatically on
# first boot, and the admin account is seeded from PB_SUPERUSER_EMAIL /
# PB_SUPERUSER_PASSWORD. Data persists in the mounted /pb_data volume.
FROM alpine:3.20
ARG PB_VERSION=0.40.4
# Docker BuildKit sets TARGETARCH (amd64/arm64) — matches PocketBase release assets.
ARG TARGETARCH
RUN apk add --no-cache curl ca-certificates unzip \
&& curl -fsSL -o /tmp/pocketbase.zip \
"https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${TARGETARCH:-amd64}.zip" \
&& unzip /tmp/pocketbase.zip -d /usr/local/bin/ \
&& rm /tmp/pocketbase.zip \
&& chmod +x /usr/local/bin/pocketbase
WORKDIR /pb
COPY pb_migrations ./pb_migrations
# All app logic (validation + email) lives in the Express API, so no runtime
# hooks are required here — an empty hooks dir keeps startup clean.
RUN mkdir -p ./pb_hooks /pb_data
EXPOSE 8090
CMD ["pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data", "--migrationsDir=/pb/pb_migrations", "--hooksDir=/pb/pb_hooks", "--encryptionEnv=PB_ENCRYPTION_KEY"]
+917
View File
@@ -0,0 +1,917 @@
/**
* This file contains TypeScript definitions for PocketBase JavaScript migrations,
* including operations with collections, records, and the $app instance.
*/
// -------------------------------------------------------------------
// Core App Interface (Transaction Context)
// -------------------------------------------------------------------
interface App {
/**
* Find a collection by its name or ID
*/
findCollectionByNameOrId(nameOrId: string): Collection;
/**
* Find an auth record by email
*/
findAuthRecordByEmail(collection: string, email: string): PBRecord;
/**
* Save a model (Collection, Record, etc.) to the database
*/
save(model: Model): void;
/**
* Save a model without validation
*/
saveNoValidate(model: Model): void;
/**
* Delete a model from the database
*/
delete(model: Model): void;
/**
* Get the app database instance for raw SQL queries
*/
db(): Database;
/**
* Get app settings
*/
settings(): Settings;
/**
* Save app settings
*/
save(settings: Settings): void;
/**
* Execute raw SQL query
*/
newQuery(sql: string): Query;
}
// -------------------------------------------------------------------
// Collection Operations
// -------------------------------------------------------------------
interface Collection {
/**
* 15 characters string to store as collection ID.
* If not set, it will be auto generated.
*/
id?: string;
/**
* Unique collection name (used as a table name for the records table).
*/
name: string;
/**
* Type of the collection.
* If not set, the collection type will be "base" by default.
*/
type?: "base" | "view" | "auth";
/**
* List with the collection fields.
* This field is optional and autopopulated for "view" collections based on the viewQuery.
*/
fields?: Array<Field>;
/**
* The collection indexes and unique constraints.
* Note that "view" collections don't support indexes.
*/
indexes?: Array<string>;
/**
* Marks the collection as "system" to prevent being renamed, deleted or modify its API rules.
*/
system?: boolean;
/**
* CRUD API rules
*/
listRule?: null | string;
viewRule?: null | string;
createRule?: null | string;
updateRule?: null | string;
deleteRule?: null | string;
// -------------------------------------------------------
// view options
// -------------------------------------------------------
/**
* Required for view collections
*/
viewQuery?: string;
// -------------------------------------------------------
// auth options
// -------------------------------------------------------
/**
* API rule that gives admin-like permissions to allow fully managing the auth record(s),
* e.g. changing the password without requiring to enter the old one, directly updating the
* verified state or email, etc. This rule is executed in addition to the createRule and updateRule.
*/
manageRule?: null | string;
/**
* API rule that could be used to specify additional record constraints applied after record
* authentication and right before returning the auth token response to the client.
*
* For example, to allow only verified users you could set it to "verified = true".
*
* Set it to empty string to allow any Auth collection record to authenticate.
*
* Set it to null to disallow authentication altogether for the collection.
*/
authRule?: null | string;
/**
* AuthAlert defines options related to the auth alerts on new device login.
*/
authAlert?: {
enabled?: boolean;
emailTemplate?: {
subject: string;
body: string;
};
};
/**
* OAuth2 specifies whether OAuth2 auth is enabled for the collection
* and which OAuth2 providers are allowed.
*/
oauth2?: {
enabled?: boolean;
mappedFields?: {
id?: string;
name?: string;
username?: string;
avatarURL?: string;
};
providers?: Array<{
name: string;
clientId: string;
clientSecret: string;
authUrl?: string;
tokenUrl?: string;
userApiUrl?: string;
displayName?: string;
pkce?: null | boolean;
}>;
};
/**
* PasswordAuth defines options related to the collection password authentication.
*/
passwordAuth?: {
enabled?: boolean;
identityFields: Array<string>;
};
/**
* MFA defines options related to the Multi-factor authentication (MFA).
*/
mfa?: {
enabled?: boolean;
duration: number;
rule?: string;
};
/**
* OTP defines options related to the One-time password authentication (OTP).
*/
otp?: {
enabled?: boolean;
duration: number;
length: number;
emailTemplate?: {
subject: string;
body: string;
};
};
/**
* Token configurations.
*/
authToken?: {
duration: number;
secret: string;
};
passwordResetToken?: {
duration: number;
secret: string;
};
emailChangeToken?: {
duration: number;
secret: string;
};
verificationToken?: {
duration: number;
secret: string;
};
fileToken?: {
duration: number;
secret: string;
};
/**
* Default email templates.
*/
verificationTemplate?: {
subject: string;
body: string;
};
resetPasswordTemplate?: {
subject: string;
body: string;
};
confirmEmailChangeTemplate?: {
subject: string;
body: string;
};
/**
* Add an index to the collection
*/
addIndex(
name: string,
unique: boolean,
columnsExpr: string,
optWhereExpr?: string
): void;
/**
* Remove an index from the collection
*/
removeIndex(name: string): void;
}
// -------------------------------------------------------------------
// Type Definitions
// -------------------------------------------------------------------
/**
* DateTime represents a date-time value
*/
type DateTime = string | Date;
// -------------------------------------------------------------------
// Base Field Interface
// -------------------------------------------------------------------
/**
* BaseField defines the common properties that all field types share
*/
interface BaseField {
/**
* Name (required) is the unique name of the field.
*/
name: string
/**
* Id is the unique stable field identifier.
*
* It is automatically generated from the name when adding to a collection FieldsList.
*/
id: string
/**
* System prevents the renaming and removal of the field.
*/
system: boolean
/**
* Hidden hides the field from the API response.
*/
hidden: boolean
/**
* Presentable hints the Dashboard UI to use the underlying
* field record value in the relation preview label.
*/
presentable: boolean
/**
* Required will require the field value to be non-empty/non-zero.
*/
required: boolean
}
// -------------------------------------------------------------------
// Field Type Definitions
// -------------------------------------------------------------------
/**
* AutodateField defines an "autodate" type field, aka.
* field which datetime value could be auto set on record create/update.
*
* This field is usually used for defining timestamp fields like "created" and "updated".
*
* Requires either both or at least one of the OnCreate or OnUpdate options to be set.
*/
interface AutodateField extends BaseField {
/**
* OnCreate auto sets the current datetime as field value on record create.
*/
onCreate: boolean
/**
* OnUpdate auto sets the current datetime as field value on record update.
*/
onUpdate: boolean
}
/**
* BoolField defines "bool" type field to store a single true/false value.
*
* The respective zero record field value is false.
*/
interface BoolField extends BaseField {
// BoolField has no additional properties beyond BaseField
// The required property from BaseField will require the field value to be always "true"
}
/**
* DateField defines "date" type field to store a single [types.DateTime] value.
*
* The respective zero record field value is the zero [types.DateTime].
*/
interface DateField extends BaseField {
/**
* Min specifies the min allowed field value.
*
* Leave it empty to skip the validator.
*/
min: DateTime
/**
* Max specifies the max allowed field value.
*
* Leave it empty to skip the validator.
*/
max: DateTime
}
/**
* EditorField defines "editor" type field to store HTML formatted text.
*
* The respective zero record field value is empty string.
*/
interface EditorField extends BaseField {
/**
* MaxSize specifies the maximum size of the allowed field value (in bytes and up to 2^53-1).
*
* If zero, a default limit of ~5MB is applied.
*/
maxSize: number
/**
* ConvertURLs is usually used to instruct the editor whether to
* apply url conversion (eg. stripping the domain name in case the
* urls are using the same domain as the one where the editor is loaded).
*
* (see also https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls)
*/
convertURLs: boolean
}
/**
* EmailField defines "email" type field for storing a single email string address.
*
* The respective zero record field value is empty string.
*/
interface EmailField extends BaseField {
/**
* ExceptDomains will require the email domain to NOT be included in the listed ones.
*
* This validator can be set only if OnlyDomains is empty.
*/
exceptDomains: Array<string>
/**
* OnlyDomains will require the email domain to be included in the listed ones.
*
* This validator can be set only if ExceptDomains is empty.
*/
onlyDomains: Array<string>
}
/**
* FileField defines "file" type field for managing record file(s).
*
* Only the file name is stored as part of the record value.
* New files (aka. files to upload) are expected to be of *filesytem.File.
*
* If MaxSelect is not set or <= 1, then the field value is expected to be a single record id.
*
* If MaxSelect is > 1, then the field value is expected to be a slice of record ids.
*
* The respective zero record field value is either empty string (single) or empty string slice (multiple).
*
* ---
*
* The following additional setter keys are available:
*
* ```
* - "fieldName+" - append one or more files to the existing record one. For example:
*
* // []string{"old1.txt", "old2.txt", "new1_ajkvass.txt", "new2_klhfnwd.txt"}
* record.Set("documents+", []*filesystem.File{new1, new2})
*
* - "+fieldName" - prepend one or more files to the existing record one. For example:
*
* // []string{"new1_ajkvass.txt", "new2_klhfnwd.txt", "old1.txt", "old2.txt",}
* record.Set("+documents", []*filesystem.File{new1, new2})
*
* - "fieldName-" - subtract/delete one or more files from the existing record one. For example:
*
* // []string{"old2.txt",}
* record.Set("documents-", "old1.txt")
* ```
*/
interface FileField extends BaseField {
/**
* MaxSize specifies the maximum size of a single uploaded file (in bytes and up to 2^53-1).
*
* If zero, a default limit of 5MB is applied.
*/
maxSize: number
/**
* MaxSelect specifies the max allowed files.
*
* For multiple files the value must be > 1, otherwise fallbacks to single (default).
*/
maxSelect: number
/**
* MimeTypes specifies an optional list of the allowed file mime types.
*
* Leave it empty to disable the validator.
*/
mimeTypes: Array<string>
/**
* Thumbs specifies an optional list of the supported thumbs for image based files.
*
* Each entry must be in one of the following formats:
*
* ```
* - WxH (eg. 100x300) - crop to WxH viewbox (from center)
* - WxHt (eg. 100x300t) - crop to WxH viewbox (from top)
* - WxHb (eg. 100x300b) - crop to WxH viewbox (from bottom)
* - WxHf (eg. 100x300f) - fit inside a WxH viewbox (without cropping)
* - 0xH (eg. 0x300) - resize to H height preserving the aspect ratio
* - Wx0 (eg. 100x0) - resize to W width preserving the aspect ratio
* ```
*/
thumbs: Array<string>
/**
* Protected will require the users to provide a special file token to access the file.
*
* Note that by default all files are publicly accessible.
*
* For the majority of the cases this is fine because by default
* all file names have random part appended to their name which
* need to be known by the user before accessing the file.
*/
protected: boolean
}
/**
* GeoPointField defines "geoPoint" type field for storing latitude and longitude GPS coordinates.
*
* You can set the record field value as [types.GeoPoint], map or serialized json object with lat-lon props.
* The stored value is always converted to [types.GeoPoint].
* Nil, empty map, empty bytes slice, etc. results in zero [types.GeoPoint].
*
* Examples of updating a record's GeoPointField value programmatically:
*
* ```
* record.Set("location", types.GeoPoint{Lat: 123, Lon: 456})
* record.Set("location", map[string]any{"lat":123, "lon":456})
* record.Set("location", []byte(`{"lat":123, "lon":456}`)
* ```
*/
interface GeoPointField extends BaseField {
// GeoPointField has no additional properties beyond BaseField
// The required property from BaseField will require the field coordinates to be non-zero (aka. not "Null Island")
}
/**
* JSONField defines "json" type field for storing any serialized JSON value.
*
* The respective zero record field value is the zero [types.JSONRaw].
*/
interface JSONField extends BaseField {
/**
* MaxSize specifies the maximum size of the allowed field value (in bytes and up to 2^53-1).
*
* If zero, a default limit of 1MB is applied.
*/
maxSize: number
}
/**
* NumberField defines "number" type field for storing numeric (float64) value.
*
* The respective zero record field value is 0.
*
* The following additional setter keys are available:
*
* ```
* - "fieldName+" - appends to the existing record value. For example:
* record.Set("total+", 5)
* - "fieldName-" - subtracts from the existing record value. For example:
* record.Set("total-", 5)
* ```
*/
interface NumberField extends BaseField {
/**
* Min specifies the min allowed field value.
*
* Leave it nil to skip the validator.
*/
min?: number
/**
* Max specifies the max allowed field value.
*
* Leave it nil to skip the validator.
*/
max?: number
/**
* OnlyInt will require the field value to be integer.
*/
onlyInt: boolean
}
/**
* PasswordField defines "password" type field for storing bcrypt hashed strings
* (usually used only internally for the "password" auth collection system field).
*
* If you want to set a direct bcrypt hash as record field value you can use the SetRaw method, for example:
*
* ```
* // generates a bcrypt hash of "123456" and set it as field value
* // (record.GetString("password") returns the plain password until persisted, otherwise empty string)
* record.Set("password", "123456")
*
* // set directly a bcrypt hash of "123456" as field value
* // (record.GetString("password") returns empty string)
* record.SetRaw("password", "$2a$10$.5Elh8fgxypNUWhpUUr/xOa2sZm0VIaE0qWuGGl9otUfobb46T1Pq")
* ```
*
* The following additional getter keys are available:
*
* ```
* - "fieldName:hash" - returns the bcrypt hash string of the record field value (if any). For example:
* record.GetString("password:hash")
* ```
*/
interface PasswordField extends BaseField {
/**
* Pattern specifies an optional regex pattern to match against the field value.
*
* Leave it empty to skip the pattern check.
*/
pattern: string
/**
* Min specifies an optional required field string length.
*/
min: number
/**
* Max specifies an optional required field string length.
*
* If zero, fallback to max 71 bytes.
*/
max: number
/**
* Cost specifies the cost/weight/iteration/etc. bcrypt factor.
*
* If zero, fallback to [bcrypt.DefaultCost].
*
* If explicitly set, must be between [bcrypt.MinCost] and [bcrypt.MaxCost].
*/
cost: number
}
/**
* RelationField defines "relation" type field for storing single or
* multiple collection record references.
*
* Requires the CollectionId option to be set.
*
* If MaxSelect is not set or <= 1, then the field value is expected to be a single record id.
*
* If MaxSelect is > 1, then the field value is expected to be a slice of record ids.
*
* The respective zero record field value is either empty string (single) or empty string slice (multiple).
*
* ---
*
* The following additional setter keys are available:
*
* ```
* - "fieldName+" - append one or more values to the existing record one. For example:
*
* record.Set("categories+", []string{"new1", "new2"}) // []string{"old1", "old2", "new1", "new2"}
*
* - "+fieldName" - prepend one or more values to the existing record one. For example:
*
* record.Set("+categories", []string{"new1", "new2"}) // []string{"new1", "new2", "old1", "old2"}
*
* - "fieldName-" - subtract one or more values from the existing record one. For example:
*
* record.Set("categories-", "old1") // []string{"old2"}
* ```
*/
interface RelationField extends BaseField {
/**
* CollectionId is the id of the related collection.
*/
collectionId: string
/**
* CascadeDelete indicates whether the root model should be deleted
* in case of delete of all linked relations.
*/
cascadeDelete: boolean
/**
* MinSelect indicates the min number of allowed relation records
* that could be linked to the main model.
*
* No min limit is applied if it is zero or negative value.
*/
minSelect: number
/**
* MaxSelect indicates the max number of allowed relation records
* that could be linked to the main model.
*
* For multiple select the value must be > 1, otherwise fallbacks to single (default).
*
* If MinSelect is set, MaxSelect must be at least >= MinSelect.
*/
maxSelect: number
}
/**
* SelectField defines "select" type field for storing single or
* multiple string values from a predefined list.
*
* Requires the Values option to be set.
*
* If MaxSelect is not set or <= 1, then the field value is expected to be a single Values element.
*
* If MaxSelect is > 1, then the field value is expected to be a subset of Values slice.
*
* The respective zero record field value is either empty string (single) or empty string slice (multiple).
*
* ---
*
* The following additional setter keys are available:
*
* ```
* - "fieldName+" - append one or more values to the existing record one. For example:
*
* record.Set("roles+", []string{"new1", "new2"}) // []string{"old1", "old2", "new1", "new2"}
*
* - "+fieldName" - prepend one or more values to the existing record one. For example:
*
* record.Set("+roles", []string{"new1", "new2"}) // []string{"new1", "new2", "old1", "old2"}
*
* - "fieldName-" - subtract one or more values from the existing record one. For example:
*
* record.Set("roles-", "old1") // []string{"old2"}
* ```
*/
interface SelectField extends BaseField {
/**
* Values specifies the list of accepted values.
*/
values: Array<string>
/**
* MaxSelect specifies the max allowed selected values.
*
* For multiple select the value must be > 1, otherwise fallbacks to single (default).
*/
maxSelect: number
}
/**
* TextField defines "text" type field for storing any string value.
*
* The respective zero record field value is empty string.
*
* The following additional setter keys are available:
*
* - "fieldName:autogenerate" - autogenerate field value if AutogeneratePattern is set. For example:
*
* ```
* record.Set("slug:autogenerate", "") // [random value]
* record.Set("slug:autogenerate", "abc-") // abc-[random value]
* ```
*/
interface TextField extends BaseField {
/**
* Min specifies the minimum required string characters.
*
* if zero value, no min limit is applied.
*/
min: number
/**
* Max specifies the maximum allowed string characters.
*
* If zero, a default limit of 5000 is applied.
*/
max: number
/**
* Pattern specifies an optional regex pattern to match against the field value.
*
* Leave it empty to skip the pattern check.
*/
pattern: string
/**
* AutogeneratePattern specifies an optional regex pattern that could
* be used to generate random string from it and set it automatically
* on record create if no explicit value is set or when the `:autogenerate` modifier is used.
*
* Note: the generated value still needs to satisfy min, max, pattern (if set)
*/
autogeneratePattern: string
/**
* PrimaryKey will mark the field as primary key.
*
* A single collection can have only 1 field marked as primary key.
*/
primaryKey: boolean
}
/**
* URLField defines "url" type field for storing a single URL string value.
*
* The respective zero record field value is empty string.
*/
interface URLField extends BaseField {
/**
* ExceptDomains will require the URL domain to NOT be included in the listed ones.
*
* This validator can be set only if OnlyDomains is empty.
*/
exceptDomains: Array<string>
/**
* OnlyDomains will require the URL domain to be included in the listed ones.
*
* This validator can be set only if ExceptDomains is empty.
*/
onlyDomains: Array<string>
}
/**
* Collection field definition
*/
interface Field {
[key: string]: any;
}
interface FieldsList extends Array<Field> {
/**
* GetByName returns a single field by its name.
*/
getByName(fieldName: string): Field
/**
* RemoveByName removes a single field by its name.
*
* This method does nothing if field with the specified name doesn't exist.
*/
removeByName(fieldName: string): void
/**
* Add adds one or more fields to the current list.
*
* By default this method will try to REPLACE existing fields with
* the new ones by their id or by their name if the new field doesn't have an explicit id.
*
* If no matching existing field is found, it will APPEND the field to the end of the list.
*
* In all cases, if any of the new fields don't have an explicit id it will auto generate a default one for them
* (the id value doesn't really matter and it is mostly used as a stable identifier in case of a field rename).
*/
add(...fields: Field[]): void
/**
* AddAt is the same as Add but insert/move the fields at the specific position.
*
* If pos < 0, then this method acts the same as calling Add.
*
* If pos > FieldsList total items, then the specified fields are inserted/moved at the end of the list.
*/
addAt(pos: number, ...fields: Field[]): void
}
// -------------------------------------------------------------------
// Record Operations
// -------------------------------------------------------------------
interface PBRecord {
/**
* Record ID
*/
id?: string;
/**
* Record data
*/
[key: string]: any;
/**
* Set a field value
*/
set(field: string, value: any): void;
/**
* Get a field value
*/
get(field: string): any;
/**
* Check if field exists
*/
has(field: string): boolean;
/**
* Remove a field
*/
remove(field: string): void;
/**
* Get all field names
*/
keys(): string[];
/**
* Get all field values
*/
values(): any[];
/**
* Get record as object
*/
export(): { [key: string]: any };
/**
* Import data into record
*/
import(data: { [key: string]: any }): void;
/**
* Refresh record from database
*/
refresh(): void;
/**
* Get collection this record belongs to
*/
collection(): Collection;
}
// -------------------------------------------------------------------
// Database Operations
// -------------------------------------------------------------------
interface Database {
/**
* Create a new query
*/
newQuery(sql: string): Query;
/**
* Execute a query
*/
execute(): any;
}
interface Query {
/**
* Execute the query
*/
execute(): any;
/**
* Bind parameters to the query
*/
bind(...params: any[]): Query;
}
// -------------------------------------------------------------------
// Settings
// -------------------------------------------------------------------
interface Settings {
/**
* App metadata
*/
meta: {
appName: string;
appURL: string;
};
}
// -------------------------------------------------------------------
// Model defines an interface with common methods that all db models should have.
// -------------------------------------------------------------------
interface Model {
[key: string]: any;
tableName(): string;
pk(): any;
lastSavedPK(): any;
isNew(): boolean;
markAsNew(): void;
markAsNotNew(): void;
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "pocketbase-app",
"type": "module",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "./pocketbase serve --http=0.0.0.0:8090 --encryptionEnv=PB_ENCRYPTION_KEY --hooksWatch=false",
"start": "./pocketbase serve --http=0.0.0.0:8090 --encryptionEnv=PB_ENCRYPTION_KEY --dir=/data --migrationsDir=./pb_migrations --hooksDir=./pb_hooks --hooksWatch=false",
"migrations:up": "./pocketbase migrate up --encryptionEnv=PB_ENCRYPTION_KEY --dir=./pb_data --migrationsDir=./pb_migrations --hooksDir=./pb_hooks --hooksWatch=false",
"migrations:revert": "./pocketbase horizons migrations:revert --encryptionEnv=PB_ENCRYPTION_KEY --dir=./pb_data --migrationsDir=./pb_migrations --hooksDir=./pb_hooks --hooksWatch=false",
"migrations:snapshot": "mkdir -p ./pb_snapshots && echo 'y' | ./pocketbase migrate collections --encryptionEnv=PB_ENCRYPTION_KEY --dir=./pb_data --migrationsDir=./pb_snapshots",
"update": "./pocketbase update --encryptionEnv=PB_ENCRYPTION_KEY --backup=0"
}
}
@@ -0,0 +1,45 @@
/// <reference path="../pb_data/types.d.ts" />
onMailerSend((e) => {
if (e.app.settings().smtp.enabled) {
return e.next()
}
const senderAddress = $os.getenv("BUILDER_MAILER_SENDER_ADDRESS");
const cc = (e.message.cc || []).map((recipient) => recipient.address).filter(Boolean);
const bcc = (e.message.bcc || []).map((recipient) => recipient.address).filter(Boolean);
const payload = {
"subject": e.message.subject,
"content": {
...(e.message.html ? {
"html": e.message.html,
} : {
"text": e.message.text,
}),
"type": "plain",
},
"from": senderAddress,
"fromName": e.message.from?.name,
"replyTo": senderAddress,
"to": e.message.to[0].address,
...(cc.length ? { "cc": cc } : {}),
...(bcc.length ? { "bcc": bcc } : {}),
}
const response = $http.send({
url: `${$os.getenv("BUILDER_MAILER_API_URL")}/api/v2/email`,
method: "POST",
headers: {
"Authorization": `Bearer ${$os.getenv("BUILDER_MAILER_API_KEY")}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (response.statusCode !== 200) {
$app.logger().error("Failed to send email", "error", response.json);
throw new ApiError(500, response.json?.message || 'Failed to send email');
}
})
@@ -0,0 +1,133 @@
/// <reference path="../pb_data/types.d.ts" />
/*
- Usage: ./pocketbase horizons migrations:revert migration1.js migration2.js ... - reverts specific migrations by name (not by count like the built-in "down" command)
*/
// Create a new "horizons" command group with a "migrations:revert" subcommand
const horizonsCmd = new Command({
use: "horizons",
short: "Additional migration utilities",
})
const exitOnError = (run) => (...args) => {
try {
return run(...args)
} catch (error) {
console.error(error)
// Command callbacks run in a panic-recovering goroutine, so throwing
// alone no longer propagates a nonzero status to the parent process.
$os.exit(1)
}
}
horizonsCmd.addCommand(new Command({
use: "migrations:revert [migration names...]",
short: "Revert specific migrations by name and delete their files",
run: exitOnError((_cmd, args) => {
if (args.length === 0) {
console.log("Usage: horizons migrations:revert <migration1.js> [migration2.js] ...")
console.log("No migrations specified to revert.")
return
}
const migrationsDir = $filepath.join(__hooks, "../pb_migrations")
const migrationFiles = {}
try {
const files = $os.readDir(migrationsDir)
for (const file of files) {
const name = file.name()
migrationFiles[name] = $filepath.join(migrationsDir, name)
}
} catch (e) {
console.error("Failed to read migrations directory:", e)
return
}
const appliedMigrations = []
for (const migrationName of args) {
if (!migrationFiles[migrationName]) {
console.error(`Migration file not found: ${migrationName}`)
continue
}
try {
const result = arrayOf(new DynamicModel({ file: "" }))
$app.db()
.select("file")
.from("_migrations")
.where($dbx.hashExp({ file: migrationName }))
.limit(1)
.all(result)
if (result.length === 0) {
console.log(`Migration ${migrationName} is not applied, skipping.`)
$os.remove(migrationFiles[migrationName]);
continue
}
appliedMigrations.push(migrationName)
} catch (e) {
console.error(`Failed to check migration status for ${migrationName}:`, e)
continue
}
}
if (appliedMigrations.length === 0) {
console.log("No applied migrations found to revert.")
return
}
console.log("\nMigrations to revert:")
for (const name of appliedMigrations) {
console.log(`${name}`)
}
const reverted = []
$app.runInTransaction((txApp) => {
for (const migrationName of appliedMigrations) {
const filePath = migrationFiles[migrationName]
try {
const content = toString($os.readFile(filePath))
// Extract the down function from migrate(up, down) call
// We need to execute the file and capture the down function
let downFn = null
const captureMigrate = (up, down) => {
downFn = down
}
// Execute the migration file with our custom migrate function since hooks doesn't have access to migrate function
const migrationCode = content.replace(/migrate\s*\(/g, '__captureMigrate(')
const fn = new Function('__captureMigrate', '$app', 'Collection', 'Record', migrationCode)
fn(captureMigrate, txApp, Collection, Record)
if (!downFn) {
console.log(`Migration ${migrationName} has no down function, just removing from history.`)
} else {
downFn(txApp)
}
txApp.db().delete("_migrations", $dbx.hashExp({ file: migrationName })).execute()
reverted.push({ name: migrationName, path: filePath })
console.log(`✓ Reverted: ${migrationName}`)
} catch (e) {
throw new Error(`failed to revert migration ${migrationName}: ${e}`)
}
}
})
for (const migration of reverted) {
$os.remove(migration.path)
}
console.log(`Successfully reverted ${reverted.length} migration(s).`)
}),
}))
$app.rootCmd.addCommand(horizonsCmd)
@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
routerAdd("GET", "/{$}", (e) => {
const fileContent = $os.readFile(`${__hooks}/../.pocketbase-version`)
const contentStr = Array.isArray(fileContent)
? String.fromCharCode.apply(null, fileContent)
: fileContent
const pbVersion = `v${contentStr.replace(/\n/g, '').trim()}`
const externalUrl = `https://horizons-static-cdn.hostinger.com/pocketbase/__PB_VERSION__/ui/dist/index.html`.replace("__PB_VERSION__", pbVersion)
try {
const response = $http.send({
url: externalUrl,
method: "GET",
timeout: 30,
})
const htmlContent = Array.isArray(response.body)
? String.fromCharCode.apply(null, response.body)
: response.body
return e.html(response.statusCode, htmlContent)
} catch (err) {
throw new NotFoundError("Failed to load dashboard", err)
}
})
@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
// Routes every _logs row to stdout/stderr on production
// Forwards every _logs row to the session journal on development and continue writing to the database
onModelCreate((e) => {
const env = $os.getenv("NODE_ENV")
if (env === "production") {
const write = e.model.level >= 8 ? console.error : console.log
try {
write(JSON.stringify({
id: e.model.id,
created: e.model.created.string(),
level: e.model.level,
message: e.model.message,
data: JSON.parse(e.model.data.string()),
}))
} catch (err) {
console.error(`Failed to forward log: ${err}`)
write(JSON.stringify({ level: e.model.level, message: e.model.message }))
}
// do not continue writing to the database
return
}
if (env === "development" || !env) {
try {
const journalPath = $filepath.join(__hooks, "..", "..", "..", "vault", "temp", "SESSION_JOURNAL.md")
const levels = { "-4": "debug", "0": "info", "4": "warn", "8": "error" }
const level = levels[String(e.model.level)] || `level${e.model.level}`
// Matches vite-plugin-session-journal's entry shape
const bullet = (key, value) => {
const text = String(value)
const indented = text.includes("\n") ? `\n ${text.replace(/\n/g, "\n ")}` : text
return `- ${key}: ${indented}\n`
}
let entry = `## ${e.model.created.string()} pocketbase.${level}\n`
entry += bullet("message", e.model.message)
const data = e.model.data.string()
if (data && data !== "{}") {
entry += bullet("data", data)
}
entry += "\n"
$os.mkdirAll($filepath.dir(journalPath), 0o755)
// $os has no append primitive
$os.cmd("sh", "-c", 'printf "%s" "$1" >> "$2"', "sh", entry, journalPath).run()
} catch (err) {
console.error(`Failed to journal log: ${err}`)
}
}
e.next()
}, "_logs")
@@ -0,0 +1,17 @@
const ALLOWED_IPS = [
"86.106.20.28", // L2
"89.116.209.21", // L2
"86.106.20.24", // L3
"84.32.173.80", // L3
"34.141.125.49", // euw3-stage-koalemos
"35.230.138.117", // euw2-prod-apollo
"35.242.131.17", // euw2-prod-apollo
"34.89.86.34", // euw2-prod-apollo
"35.242.132.122", // euw2-prod-apollo
"35.234.116.158", // euw3-stage-koalemos
"34.159.186.242", // euw3-stage-koalemos
]
module.exports = {
ALLOWED_IPS,
}
+101
View File
@@ -0,0 +1,101 @@
/// <reference path="../pb_data/types.d.ts" />
onRecordCreateRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to create superuser`)
}
e.next()
}, "_superusers")
onRecordUpdateRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to update superuser`)
}
e.next()
}, "_superusers")
onRecordDeleteRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to delete superuser`)
}
e.next()
}, "_superusers")
onRecordRequestPasswordResetRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to request password reset for superuser`)
}
e.next()
}, "_superusers")
onCollectionViewRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to view collection`)
}
e.next()
})
onCollectionCreateRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to create collection`)
}
e.next()
})
onCollectionUpdateRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to update collection`)
}
e.next()
})
onCollectionDeleteRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to delete collection`)
}
e.next()
})
onCollectionsImportRequest((e) => {
const { ALLOWED_IPS } = require(`${__hooks}/superusers-allow-list.js`)
const ip = e.realIP()
if (!ALLOWED_IPS.includes(ip)) {
throw new BadRequestError(`Not allowed to import collections`)
}
e.next()
})
@@ -0,0 +1,20 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
let settings = app.settings()
settings.meta.appName = "f7b2aa49-1077-4692-8b37-dae2a255116c.app-preview.com"
settings.meta.appURL = "https://f7b2aa49-1077-4692-8b37-dae2a255116c.app-preview.com/hcgi/platform"
settings.meta.hideControls = true
settings.logs.maxDays = 7
settings.logs.minLevel = 8
settings.logs.logIP = true
settings.trustedProxy.headers = [
"X-Real-IP",
"X-Forwarded-For",
"CF-Connecting-IP",
]
app.save(settings)
})
@@ -0,0 +1,10 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const superusers = app.findCollectionByNameOrId("_superusers")
const record = new Record(superusers)
record.set("email", $os.getenv("PB_SUPERUSER_EMAIL"))
record.set("password", $os.getenv("PB_SUPERUSER_PASSWORD"))
app.save(record)
})
@@ -0,0 +1,10 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const superusers = app.findCollectionByNameOrId("_superusers")
superusers.authAlert = {
enabled: false,
}
app.save(superusers)
})
@@ -0,0 +1,55 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
let settings = app.settings()
settings.rateLimits = {
enabled: true,
rules: [
// General API limit - 200 requests per 5 minutes
{
label: "/api",
audience: "",
duration: 5 * 60, // 5 minutes
maxRequests: 200
},
// Brute force protection for auth attempts - 20 attempts per 5 minutes (guests only)
{
label: "*:auth",
audience: "@guest", // Only for unauthenticated users
duration: 5 * 60, // 15 minutes
maxRequests: 20
},
// Password reset - 5 per hour
{
label: "POST /api/collections/users/request-password-reset",
audience: "",
duration: 60 * 60, // 1 hour
maxRequests: 5
},
// Email verification - 5 per hour
{
label: "POST /api/collections/users/request-verification",
audience: "",
duration: 60 * 60, // 1 hour
maxRequests: 5
},
// Email change - 3 per hour (authenticated only)
{
label: "POST /api/collections/users/request-email-change",
audience: "@auth",
duration: 60 * 60, // 1 hour
maxRequests: 3
},
// OTP - 10 per hour
{
label: "POST /api/collections/users/request-otp",
audience: "",
duration: 60 * 60, // 1 hour
maxRequests: 10
}
]
}
app.save(settings)
})
@@ -0,0 +1,19 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
let usersCollection
try {
usersCollection = app.findCollectionByNameOrId("users")
usersCollection.authAlert = {
enabled: false,
}
} catch (error) {
if (error.message.includes("no rows in result set")) {
return
}
throw error
}
app.save(usersCollection)
})
@@ -0,0 +1,159 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text9973129546",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "text7224323192",
"name": "firstName",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text8927180967",
"name": "lastName",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "email3000958229",
"name": "email",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "email",
"exceptDomains": [],
"onlyDomains": []
},
{
"hidden": false,
"id": "json4467574208",
"name": "websites",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "json",
"maxSize": 0
},
{
"hidden": false,
"id": "json7565497650",
"name": "apps",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "json",
"maxSize": 0
},
{
"hidden": false,
"id": "json2686026609",
"name": "systems",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "json",
"maxSize": 0
},
{
"hidden": false,
"id": "text2579574645",
"name": "industry",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "autodate2823430259",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate7741794431",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_3506166606",
"indexes": [],
"listRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"name": "assessments",
"system": false,
"type": "base",
"updateRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"viewRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'"
});
try {
return app.save(collection);
} catch (e) {
if (e.message.includes("Collection name must be unique")) {
console.log("Collection already exists, skipping");
return;
}
throw e;
}
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("pbc_3506166606");
return app.delete(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("websites");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "websites",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("apps");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "apps",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("systems");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "systems",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("websites");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "websites",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("apps");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "apps",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("systems");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "systems",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,31 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const existing = collection.fields.getByName("websites_text");
if (existing) {
if (existing.type === "text") {
return; // field already exists with correct type, skip
}
collection.fields.removeByName("websites_text"); // exists with wrong type, remove first
}
collection.fields.add(new TextField({
name: "websites_text",
required: true
}));
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("websites_text");
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,31 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const existing = collection.fields.getByName("apps_text");
if (existing) {
if (existing.type === "text") {
return; // field already exists with correct type, skip
}
collection.fields.removeByName("apps_text"); // exists with wrong type, remove first
}
collection.fields.add(new TextField({
name: "apps_text",
required: true
}));
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("apps_text");
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,31 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const existing = collection.fields.getByName("systems_text");
if (existing) {
if (existing.type === "text") {
return; // field already exists with correct type, skip
}
collection.fields.removeByName("systems_text"); // exists with wrong type, remove first
}
collection.fields.add(new TextField({
name: "systems_text",
required: true
}));
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("systems_text");
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("websites");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "websites",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("apps");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "apps",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("systems");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "systems",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("websites");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "websites",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("apps");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "apps",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.removeByName("systems");
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
collection.fields.add(new JSONField({
name: "systems",
required: false
}));
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,16 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
return app.delete(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping delete");
return;
}
throw e;
}
}, (app) => {
// Note: The down migration cannot restore data, only the collection structure
// You would need to manually recreate the collection structure here if needed
})
@@ -0,0 +1,16 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
return app.delete(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping delete");
return;
}
throw e;
}
}, (app) => {
// Note: The down migration cannot restore data, only the collection structure
// You would need to manually recreate the collection structure here if needed
})
@@ -0,0 +1,170 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3930877004",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "text9496940303",
"name": "firstName",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text6526030650",
"name": "lastName",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text1504649217",
"name": "email",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text3766238150",
"name": "industry",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text0206998537",
"name": "websites",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text1142803244",
"name": "applications",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "text6766822077",
"name": "systems",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text",
"autogeneratePattern": "",
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "autodate1878312857",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate5206414872",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_6716256996",
"indexes": [],
"listRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"name": "assessments",
"system": false,
"type": "base",
"updateRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'",
"viewRule": "@request.auth.collectionName = 'users' && @request.auth.role = 'admin'"
});
try {
return app.save(collection);
} catch (e) {
if (e.message.includes("Collection name must be unique")) {
console.log("Collection already exists, skipping");
return;
}
throw e;
}
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("pbc_6716256996");
return app.delete(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,21 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("websites");
field.required = false;
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("websites");
if (!field) { console.log("Field not found, skipping revert"); return; }
field.required = true;
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection or field not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,21 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("applications");
field.required = false;
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("applications");
if (!field) { console.log("Field not found, skipping revert"); return; }
field.required = true;
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection or field not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,21 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("systems");
field.required = false;
return app.save(collection);
}, (app) => {
try {
const collection = app.findCollectionByNameOrId("assessments");
const field = collection.fields.getByName("systems");
if (!field) { console.log("Field not found, skipping revert"); return; }
field.required = true;
return app.save(collection);
} catch (e) {
if (e.message.includes("no rows in result set")) {
console.log("Collection or field not found, skipping revert");
return;
}
throw e;
}
})
@@ -0,0 +1,32 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("assessments");
const record0 = new Record(collection);
record0.id = "zlolg0zav7yhepz";
record0.set("firstName", "John");
record0.set("lastName", "Doe");
record0.set("email", "john@example.com");
record0.set("industry", "Technology");
try {
app.save(record0);
} catch (e) {
if (e.message.includes("Value must be unique")) {
console.log("Record with unique value already exists, skipping");
} else {
throw e;
}
}
}, (app) => {
const seededRecordIds = ["zlolg0zav7yhepz"];
for (const seededRecordId of seededRecordIds) {
try {
app.delete(app.findRecordById("assessments", seededRecordId));
} catch (error) {
if (error.message.includes("no rows in result set")) {
continue;
}
throw error;
}
}
})