Initial commit: Korrith website, assessment API, PocketBase schema, Docker/Coolify deployment
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
pb_data
|
||||
pb_backups
|
||||
pb_snapshots
|
||||
pocketbase
|
||||
.pocketbase-sha256
|
||||
database-types.d.ts
|
||||
@@ -0,0 +1 @@
|
||||
66396ed5efec837aa8d2addc009894968a5f4fb7cb2b0ea57db6105cb91b095d
|
||||
@@ -0,0 +1 @@
|
||||
0.40.4
|
||||
@@ -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"]
|
||||
Vendored
+917
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user