Fix Garmin token storage and flaky e2e test

1. Increase garminOauth1Token and garminOauth2Token max length from
   5000 to 20000 characters to accommodate encrypted OAuth tokens.
   Add logic to update existing field constraints in addUserFields().

2. Fix flaky pocketbase-harness e2e test by adding retry logic with
   exponential backoff to createAdminUser() and createTestUser().
   Handles SQLite database lock during PocketBase startup migrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 12:52:01 +00:00
parent 00b84d0b22
commit 6df145d916
3 changed files with 166 additions and 47 deletions

View File

@@ -6,10 +6,12 @@ import PocketBase from "pocketbase";
* Collection field definition for PocketBase.
* For relation fields, collectionId/maxSelect/cascadeDelete are top-level properties.
*/
interface CollectionField {
export interface CollectionField {
name: string;
type: string;
required?: boolean;
// Text field max length (PocketBase defaults to 5000 if not specified)
max?: number;
// Relation field properties (top-level, not in options)
collectionId?: string;
maxSelect?: number;
@@ -142,10 +144,10 @@ const REQUIRED_COLLECTIONS = [PERIOD_LOGS_COLLECTION, DAILY_LOGS_COLLECTION];
* Custom fields to add to the users collection.
* These are required for Garmin integration and app functionality.
*/
const USER_CUSTOM_FIELDS = [
export const USER_CUSTOM_FIELDS: CollectionField[] = [
{ name: "garminConnected", type: "bool" },
{ name: "garminOauth1Token", type: "text" },
{ name: "garminOauth2Token", type: "text" },
{ name: "garminOauth1Token", type: "text", max: 20000 },
{ name: "garminOauth2Token", type: "text", max: 20000 },
{ name: "garminTokenExpiresAt", type: "date" },
{ name: "calendarToken", type: "text" },
{ name: "lastPeriodDate", type: "date" },
@@ -156,36 +158,62 @@ const USER_CUSTOM_FIELDS = [
];
/**
* Adds custom fields to the users collection if they don't already exist.
* Adds or updates custom fields on the users collection.
* For new fields: adds them. For existing fields: updates max constraint if different.
* This is idempotent - safe to run multiple times.
*/
export async function addUserFields(pb: PocketBase): Promise<void> {
const usersCollection = await pb.collections.getOne("users");
// Get existing field names
const existingFieldNames = new Set(
(usersCollection.fields || []).map((f: { name: string }) => f.name),
// Build a map of existing fields by name
const existingFieldsMap = new Map<string, Record<string, unknown>>(
(usersCollection.fields || []).map((f: Record<string, unknown>) => [
f.name as string,
f,
]),
);
// Filter to only new fields
const newFields = USER_CUSTOM_FIELDS.filter(
(f) => !existingFieldNames.has(f.name),
);
// Separate new fields from fields that need updating
const newFields: CollectionField[] = [];
const fieldsToUpdate: string[] = [];
if (newFields.length > 0) {
// Combine existing fields with new ones
for (const definedField of USER_CUSTOM_FIELDS) {
const existingField = existingFieldsMap.get(definedField.name);
if (!existingField) {
newFields.push(definedField);
} else if (
definedField.max !== undefined &&
existingField.max !== definedField.max
) {
fieldsToUpdate.push(definedField.name);
existingField.max = definedField.max;
}
}
const hasChanges = newFields.length > 0 || fieldsToUpdate.length > 0;
if (hasChanges) {
// Combine existing fields (with updates) and new fields
const allFields = [...(usersCollection.fields || []), ...newFields];
await pb.collections.update(usersCollection.id, {
fields: allFields,
});
console.log(
` Added ${newFields.length} field(s) to users:`,
newFields.map((f) => f.name),
);
if (newFields.length > 0) {
console.log(
` Added ${newFields.length} field(s) to users:`,
newFields.map((f) => f.name),
);
}
if (fieldsToUpdate.length > 0) {
console.log(
` Updated max constraint for ${fieldsToUpdate.length} field(s):`,
fieldsToUpdate,
);
}
} else {
console.log(" All user fields already exist.");
console.log(" All user fields already exist with correct settings.");
}
}