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

140
docs/development/domain-configuration.md

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

3
src/constants/app.ts

@ -47,9 +47,6 @@ export const DEFAULT_PARTNER_API_SERVER =
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_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 IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED = export const PASSKEYS_ENABLED =

11
src/db/tables/contacts.ts

@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string; 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 = { export const ContactSchema = {
contacts: "&did, name", // no need to key by other things contacts: "&did, name", // no need to key by other things
}; };

118
src/utils/PlatformServiceMixin.ts

@ -50,7 +50,7 @@ import {
type SettingsWithJsonStrings, type SettingsWithJsonStrings,
} from "@/db/tables/settings"; } from "@/db/tables/settings";
import { logger } from "@/utils/logger"; 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 { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp"; import { Temp } from "@/db/tables/temp";
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database"; import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
@ -642,15 +642,81 @@ export const PlatformServiceMixin = {
// CACHED SPECIALIZED SHORTCUTS (massive performance boost) // 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() * Load all contacts (always fresh) - $contacts()
* Always fetches fresh data from database for consistency * 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[]> { async $contacts(): Promise<Contact[]> {
return (await this.$query( const rawContacts = (await this.$query(
"SELECT * FROM contacts ORDER BY name", "SELECT * FROM contacts ORDER BY name",
)) as Contact[]; )) as ContactMaybeWithJsonStrings[];
return this.$normalizeContacts(rawContacts);
}, },
/** /**
@ -1026,7 +1092,13 @@ export const PlatformServiceMixin = {
Object.entries(changes).forEach(([key, value]) => { Object.entries(changes).forEach(([key, value]) => {
if (value !== undefined) { if (value !== undefined) {
setParts.push(`${key} = ?`); setParts.push(`${key} = ?`);
params.push(value);
// Handle contactMethods field - convert array to JSON string
if (key === "contactMethods" && Array.isArray(value)) {
params.push(JSON.stringify(value));
} else {
params.push(value);
}
} }
}); });
@ -1048,45 +1120,36 @@ export const PlatformServiceMixin = {
/** /**
* Get all contacts as typed objects - $getAllContacts() * Get all contacts as typed objects - $getAllContacts()
* Eliminates verbose query + mapping patterns * 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[]> { async $getAllContacts(): Promise<Contact[]> {
const results = await this.$dbQuery( const rawContacts = (await this.$query(
"SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name", "SELECT * FROM contacts ORDER BY name",
); )) as ContactMaybeWithJsonStrings[];
return this.$mapResults(results, (row: unknown[]) => ({ return this.$normalizeContacts(rawContacts);
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,
}));
}, },
/** /**
* Get single contact by DID - $getContact() * Get single contact by DID - $getContact()
* Eliminates verbose single contact query patterns * Eliminates verbose single contact query patterns
* Handles JSON string/object duality for contactMethods field
* @param did Contact DID to retrieve * @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> { async $getContact(did: string): Promise<Contact | null> {
const results = await this.$dbQuery( const rawContacts = (await this.$query(
"SELECT * FROM contacts WHERE did = ?", "SELECT * FROM contacts WHERE did = ?",
[did], [did],
); )) as ContactMaybeWithJsonStrings[];
if (!results || !results.values || results.values.length === 0) { if (rawContacts.length === 0) {
return null; return null;
} }
const contactData = this._mapColumnsToValues( const normalizedContacts = this.$normalizeContacts(rawContacts);
results.columns, return normalizedContacts[0];
results.values,
);
return contactData.length > 0 ? (contactData[0] as Contact) : null;
}, },
/** /**
@ -1681,6 +1744,7 @@ declare module "@vue/runtime-core" {
$contactCount(): Promise<number>; $contactCount(): Promise<number>;
$settings(defaults?: Settings): Promise<Settings>; $settings(defaults?: Settings): Promise<Settings>;
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>; $accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
// Settings update shortcuts (eliminate 90% boilerplate) // Settings update shortcuts (eliminate 90% boilerplate)
$saveSettings(changes: Partial<Settings>): Promise<boolean>; $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.contact = contact;
this.contactName = contact.name || ""; this.contactName = contact.name || "";
this.contactNotes = contact.notes || ""; 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 { } else {
this.notify.error( this.notify.error(
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`, `${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,

Loading…
Cancel
Save