Browse Source

Merge branch 'build-improvement' into performance-optimizations-testing

pull/159/head
Matthew Raymer 3 weeks ago
parent
commit
9bfa439e9c
  1. 8
      README.md
  2. 140
      docs/development/domain-configuration.md
  3. 3
      src/constants/app.ts
  4. 11
      src/db/tables/contacts.ts
  5. 116
      src/utils/PlatformServiceMixin.ts
  6. 16
      src/views/ContactEditView.vue

8
README.md

@ -113,10 +113,11 @@ appearing in shared links during development.
- ✅ **Type-Safe Configuration**: Full TypeScript support
### Quick Reference
```typescript
// For sharing functionality (always production)
import { PROD_SHARE_DOMAIN } from "@/constants/app";
const shareLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
// For sharing functionality (environment-specific)
import { APP_SERVER } from "@/constants/app";
const shareLink = `${APP_SERVER}/deep-link/claim/123`;
// For internal operations (environment-specific)
import { APP_SERVER } from "@/constants/app";
@ -124,6 +125,7 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
```
### Documentation
- [Domain Configuration System](docs/domain-configuration.md) - Complete guide
- [Constants and Configuration](src/constants/app.ts) - Core constants

140
docs/development/domain-configuration.md

@ -2,33 +2,30 @@
**Author**: Matthew Raymer
**Date**: 2025-01-27
**Status**: ✅ **COMPLETE** - Domain configuration system implemented
**Status**: ✅ **UPDATED** - Simplified to use APP_SERVER for all functionality
## Overview
TimeSafari uses a centralized domain configuration system to ensure consistent
URL generation across all environments. This system prevents localhost URLs from
appearing in shared links during development and provides a single point of
control for domain changes.
URL generation across all environments. This system provides a single point of
control for domain changes and uses environment-specific configuration for all
functionality including sharing.
## Problem Solved
### Issue: Localhost URLs in Shared Links
### Issue: Inconsistent Domain Usage
Previously, copy link buttons and deep link generation used the environment-
specific `APP_SERVER` constant, which resulted in:
Previously, the system used separate constants for different types of URLs:
- **Development**: `http://localhost:8080/deep-link/claim/123`
- **Test**: `https://test.timesafari.app/deep-link/claim/123`
- **Production**: `https://timesafari.app/deep-link/claim/123`
- **Internal Operations**: Used `APP_SERVER` (environment-specific)
- **Sharing**: Used separate constants (removed)
This caused problems when users in development mode shared links, as the
localhost URLs wouldn't work for other users.
This created complexity and confusion about when to use which constant.
### Solution: Production Domain for Sharing
### Solution: Unified Domain Configuration
All sharing functionality now uses the `PROD_SHARE_DOMAIN` constant, which
always points to the production domain regardless of the current environment.
All functionality now uses the `APP_SERVER` constant, which provides
environment-specific URLs that can be configured per environment.
## Implementation
@ -43,27 +40,28 @@ export enum AppString {
// ... other constants ...
}
// Production domain for sharing links (always use production URL for sharing)
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
// Environment-specific server URL for all functionality
export const APP_SERVER =
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
```
### Usage Pattern
All components that generate shareable links follow this pattern:
All components that generate URLs follow this pattern:
```typescript
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import { APP_SERVER } from "@/constants/app";
// In component class
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
APP_SERVER = APP_SERVER;
// In methods
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/${claimId}`;
const deepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
```
### Components Updated
The following components and services were updated to use `PROD_SHARE_DOMAIN`:
The following components and services use `APP_SERVER`:
#### Views
- `ClaimView.vue` - Claim and certificate links
@ -82,17 +80,28 @@ The following components and services were updated to use `PROD_SHARE_DOMAIN`:
## Configuration Management
### Changing the Production Domain
### Environment-Specific Configuration
To change the production domain for all sharing functionality:
The system uses environment variables to configure domains:
1. **Update the constant** in `src/constants/app.ts`:
```typescript
export enum AppString {
// ... other constants ...
PROD_PUSH_SERVER = "https://your-new-domain.com",
// ... other constants ...
}
```bash
# Development
VITE_APP_SERVER=http://localhost:8080
# Test
VITE_APP_SERVER=https://test.timesafari.app
# Production
VITE_APP_SERVER=https://timesafari.app
```
### Changing the Domain
To change the domain for all functionality:
1. **Update environment variables** for the target environment:
```bash
VITE_APP_SERVER=https://your-new-domain.com
```
2. **Rebuild the application** for all platforms:
@ -102,46 +111,32 @@ To change the production domain for all sharing functionality:
npm run build:electron
```
### Environment-Specific Configuration
The system maintains environment-specific configuration for internal operations
while using production domains for sharing:
```typescript
// Internal operations use environment-specific URLs
export const APP_SERVER =
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
## Benefits
// Sharing always uses production URLs
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
```
### ✅ Simplified Configuration
## Benefits
- Single constant for all URL generation
- No confusion about which constant to use
- Consistent behavior across all functionality
### ✅ Consistent User Experience
### ✅ Environment Flexibility
- All shared links work for all users regardless of environment
- No more broken localhost links in development
- Consistent behavior across all platforms
- Easy to configure different domains per environment
- Support for development, test, and production environments
- Environment-specific sharing URLs when needed
### ✅ Maintainability
- Single source of truth for production domain
- Single source of truth for domain configuration
- Easy to change domain across entire application
- Clear separation between internal and sharing URLs
- Clear pattern for implementing new URL functionality
### ✅ Developer Experience
- No need to remember which environment URLs work for sharing
- Clear pattern for implementing new sharing functionality
- Simple, consistent pattern for URL generation
- Clear documentation and examples
- Type-safe configuration with TypeScript
### ✅ Security
- No accidental exposure of internal development URLs
- Controlled domain configuration
- Clear audit trail for domain changes
## Testing
### Manual Testing
@ -150,7 +145,7 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
```bash
npm run dev
# Navigate to any page with copy link buttons
# Verify links use production domain, not localhost
# Verify links use configured domain
```
2. **Production Build**:
@ -164,27 +159,19 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
The implementation includes comprehensive linting to ensure:
- All components properly import `PROD_SHARE_DOMAIN`
- No hardcoded URLs in sharing functionality
- All components properly import `APP_SERVER`
- No hardcoded URLs in functionality
- Consistent usage patterns across the codebase
## Migration Notes
## Implementation Pattern
### Before Implementation
### Current Approach
```typescript
// ❌ Hardcoded URLs
const deepLink = "https://timesafari.app/deep-link/claim/123";
// ❌ Environment-specific URLs
const deepLink = `${APP_SERVER}/deep-link/claim/123`;
```
### After Implementation
```typescript
// ✅ Configurable production URLs
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
// ✅ Single constant for all functionality
import { APP_SERVER } from "@/constants/app";
const shareLink = `${APP_SERVER}/deep-link/claim/123`;
const apiUrl = `${APP_SERVER}/api/claim/123`;
```
## Future Enhancements
@ -208,6 +195,7 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
```
3. **Platform-Specific Domains**:
```typescript
export const getPlatformShareDomain = () => {
const platform = process.env.VITE_PLATFORM;
@ -229,5 +217,5 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`;
---
**Last Updated**: 2025-01-27
**Version**: 1.0
**Version**: 2.0
**Maintainer**: Matthew Raymer

3
src/constants/app.ts

@ -47,9 +47,6 @@ export const DEFAULT_PARTNER_API_SERVER =
export const DEFAULT_PUSH_SERVER =
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
// Production domain for sharing links (always use production URL for sharing)
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =

11
src/db/tables/contacts.ts

@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string;
};
/**
* This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values.
* See src/db/databaseUtil.ts parseJsonField for more details.
*
* This is so that we can reuse most of the type and don't have to maintain another copy.
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
*/
export type ContactMaybeWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string | Array<ContactMethod>;
};
export const ContactSchema = {
contacts: "&did, name", // no need to key by other things
};

116
src/utils/PlatformServiceMixin.ts

@ -50,7 +50,7 @@ import {
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
import { logger } from "@/utils/logger";
import { Contact } from "@/db/tables/contacts";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp";
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
@ -642,15 +642,81 @@ export const PlatformServiceMixin = {
// CACHED SPECIALIZED SHORTCUTS (massive performance boost)
// =================================================
/**
* Normalize contact data by parsing JSON strings into proper objects
* Handles the contactMethods field which can be either a JSON string or an array
* @param rawContacts Raw contact data from database
* @returns Normalized Contact[] array
*/
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[] {
return rawContacts.map((contact) => {
// Create a new contact object with proper typing
const normalizedContact: Contact = {
did: contact.did,
iViewContent: contact.iViewContent,
name: contact.name,
nextPubKeyHashB64: contact.nextPubKeyHashB64,
notes: contact.notes,
profileImageUrl: contact.profileImageUrl,
publicKeyBase64: contact.publicKeyBase64,
seesMe: contact.seesMe,
registered: contact.registered,
};
// Handle contactMethods field which can be a JSON string or an array
if (contact.contactMethods !== undefined) {
if (typeof contact.contactMethods === "string") {
// Parse JSON string into array
normalizedContact.contactMethods = this._parseJsonField(
contact.contactMethods,
[],
);
} else if (Array.isArray(contact.contactMethods)) {
// Validate that each item in the array is a proper ContactMethod object
normalizedContact.contactMethods = contact.contactMethods.filter(
(method) => {
const isValid =
method &&
typeof method === "object" &&
typeof method.label === "string" &&
typeof method.type === "string" &&
typeof method.value === "string";
if (!isValid && method !== undefined) {
console.warn(
"[ContactNormalization] Invalid contact method:",
method,
);
}
return isValid;
},
);
} else {
// Invalid data, use empty array
normalizedContact.contactMethods = [];
}
} else {
// No contactMethods, use empty array
normalizedContact.contactMethods = [];
}
return normalizedContact;
});
},
/**
* Load all contacts (always fresh) - $contacts()
* Always fetches fresh data from database for consistency
* @returns Promise<Contact[]> Array of contact objects
* Handles JSON string/object duality for contactMethods field
* @returns Promise<Contact[]> Array of normalized contact objects
*/
async $contacts(): Promise<Contact[]> {
return (await this.$query(
const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY name",
)) as Contact[];
)) as ContactMaybeWithJsonStrings[];
return this.$normalizeContacts(rawContacts);
},
/**
@ -1026,8 +1092,14 @@ export const PlatformServiceMixin = {
Object.entries(changes).forEach(([key, value]) => {
if (value !== undefined) {
setParts.push(`${key} = ?`);
// Handle contactMethods field - convert array to JSON string
if (key === "contactMethods" && Array.isArray(value)) {
params.push(JSON.stringify(value));
} else {
params.push(value);
}
}
});
if (setParts.length === 0) return true;
@ -1048,45 +1120,36 @@ export const PlatformServiceMixin = {
/**
* Get all contacts as typed objects - $getAllContacts()
* Eliminates verbose query + mapping patterns
* @returns Promise<Contact[]> Array of contact objects
* Handles JSON string/object duality for contactMethods field
* @returns Promise<Contact[]> Array of normalized contact objects
*/
async $getAllContacts(): Promise<Contact[]> {
const results = await this.$dbQuery(
"SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name",
);
const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY name",
)) as ContactMaybeWithJsonStrings[];
return this.$mapResults(results, (row: unknown[]) => ({
did: row[0] as string,
name: row[1] as string,
publicKeyBase64: row[2] as string,
seesMe: Boolean(row[3]),
registered: Boolean(row[4]),
nextPubKeyHashB64: row[5] as string,
profileImageUrl: row[6] as string,
}));
return this.$normalizeContacts(rawContacts);
},
/**
* Get single contact by DID - $getContact()
* Eliminates verbose single contact query patterns
* Handles JSON string/object duality for contactMethods field
* @param did Contact DID to retrieve
* @returns Promise<Contact | null> Contact object or null if not found
* @returns Promise<Contact | null> Normalized contact object or null if not found
*/
async $getContact(did: string): Promise<Contact | null> {
const results = await this.$dbQuery(
const rawContacts = (await this.$query(
"SELECT * FROM contacts WHERE did = ?",
[did],
);
)) as ContactMaybeWithJsonStrings[];
if (!results || !results.values || results.values.length === 0) {
if (rawContacts.length === 0) {
return null;
}
const contactData = this._mapColumnsToValues(
results.columns,
results.values,
);
return contactData.length > 0 ? (contactData[0] as Contact) : null;
const normalizedContacts = this.$normalizeContacts(rawContacts);
return normalizedContacts[0];
},
/**
@ -1681,6 +1744,7 @@ declare module "@vue/runtime-core" {
$contactCount(): Promise<number>;
$settings(defaults?: Settings): Promise<Settings>;
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
// Settings update shortcuts (eliminate 90% boilerplate)
$saveSettings(changes: Partial<Settings>): Promise<boolean>;

16
src/views/ContactEditView.vue

@ -239,7 +239,21 @@ export default class ContactEditView extends Vue {
this.contact = contact;
this.contactName = contact.name || "";
this.contactNotes = contact.notes || "";
this.contactMethods = contact.contactMethods || [];
// Ensure contactMethods is a valid array of ContactMethod objects
if (Array.isArray(contact.contactMethods)) {
this.contactMethods = contact.contactMethods.filter((method) => {
return (
method &&
typeof method === "object" &&
typeof method.label === "string" &&
typeof method.type === "string" &&
typeof method.value === "string"
);
});
} else {
this.contactMethods = [];
}
} else {
this.notify.error(
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,

Loading…
Cancel
Save