longurl-js
Version:
LongURL - Programmable URL management framework with entity-driven design and production-ready infrastructure
539 lines (459 loc) • 20.4 kB
Markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.4] - 2025-11-10
### Changed
- **OPTIMIZED**: QR codes now use short URLs for faster scanning (like Bitly)
- QR codes now encode `url_slug_short` (e.g., `https://yourdomain.co/X7gT5p`) instead of full framework URLs
- Reduces QR code complexity from 60-80 characters to ~25 characters
- Faster scanning, better error correction, works better in low-light conditions
- Framework URLs still available for SEO; short URLs redirect via resolver (application-level)
- Applies to all generation modes: framework mode, pattern mode, and shortening mode
## [0.5.3] - 2025-11-10
### Fixed
- **FIXED**: Type definitions alignment
- `LongURLConfig.supabase.options` now matches `SupabaseConfig.options` structure
- Added missing `realTime` property to `LongURLConfig`
- Added `qr_code_url`, `qr_code`, and `url_slug_short` fields to `EndpointRecord` interface
- All storage configuration types now properly exported and aligned
## [0.5.2] - 2025-11-10
### Changed
- **UPDATED**: QR code bucket storage path pattern
- QR codes now stored using `{entityType}/{entityId}.png` pattern
- Example: `product/550e8400-e29b-41d4-a716-446655440000.png`
- Organized folder structure by entity type
- Uses entity UUID from endpoints table for consistent naming
## [0.5.1] - 2025-11-10
### Added
- **NEW**: QR code bucket storage (default behavior)
- QR codes now uploaded to Supabase Storage bucket by default
- Stores QR code URL in `qr_code_url` column instead of base64 in `qr_code`
- Default bucket: `qr-codes` (configurable via `options.storage.qrCodeBucket`)
- More efficient: No base64 bloat in database, faster queries
- Better performance: CDN-delivered QR codes from storage bucket
- **NEW**: Optional table storage for QR codes
- Set `options.storage.storeQRInTable: true` to store base64 in `qr_code` column (old behavior)
- Allows opt-in to legacy base64 storage if needed
- Default: `false` (uses bucket storage)
### Changed
- **ENHANCED**: QR code storage architecture
- **Default**: Upload QR codes to Supabase Storage bucket (`qr-codes`)
- **Optional**: Store base64 in `qr_code` column (opt-in via config)
- Response includes `qrCodeUrl` (bucket URL) by default
- Response includes `qrCode` (base64) only if `storeQRInTable: true`
- **IMPROVED**: `SupabaseAdapter.save()` now handles QR code upload
- Automatically uploads QR codes to configured bucket
- Stores public URL in `qr_code_url` column
- Handles upload errors gracefully (throws clear error messages)
- Updates cache with `qrCodeUrl` after successful upload
### Configuration
```typescript
// Default: Bucket storage (new, efficient)
const longurl = new LongURL({
supabase: {
url: '...',
key: '...',
options: {
storage: {
qrCodeBucket: 'qr-codes' // Optional: default is 'qr-codes'
}
}
}
});
// QR codes uploaded to bucket, URL stored in qr_code_url
// Optional: Table storage (old behavior, opt-in)
const longurl = new LongURL({
supabase: {
url: '...',
key: '...',
options: {
storage: {
storeQRInTable: true // Opt-in to base64 storage
}
}
}
});
// QR codes stored as base64 in qr_code column
```
### Database
- **REQUIRED**: Database must have `qr_code_url` column (already added in your Supabase setup)
- No migration needed if column already exists
- `qr_code` column still supported for backward compatibility (when `storeQRInTable: true`)
### Examples
#### Default bucket storage
```typescript
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...');
// result.qrCodeUrl = "https://...supabase.co/storage/v1/object/public/qr-codes/X7gT5p.png"
// result.qrCode = undefined
```
#### Opt-in table storage
```typescript
const longurl = new LongURL({
supabase: {
url: '...',
key: '...',
options: {
storage: { storeQRInTable: true }
}
}
});
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...');
// result.qrCode = "data:image/png;base64,..."
// result.qrCodeUrl = undefined
```
## [0.5.0] - 2025-11-10
### Added
- **NEW**: Update/Upsert functionality for existing endpoints
- `manageUrl()` now supports updating existing endpoints by entity
- Automatic upsert: INSERT if new, UPDATE if entity exists
- Entity-based lookup: Updates by `entity_type + entity_id` (not just slug)
- Metadata merging: Preserves existing metadata, adds new fields
- Slug change support: Can update `url_slug` when URL pattern changes
- Collision protection: Prevents duplicate slugs across entities
### Changed
- **ENHANCED**: `SupabaseAdapter.save()` now implements upsert logic
- Checks if entity exists before insert
- Updates existing rows instead of failing on duplicates
- Preserves `created_at` timestamp on updates
- Updates `updated_at` timestamp automatically
- Merges metadata instead of replacing
- **IMPROVED**: Error messages for collision detection
- Clear messages when slug collisions occur
- Suggests using update() or different entity_id
- Distinguishes between update conflicts and create conflicts
### Behavior
- **Idempotent**: Calling `manageUrl()` multiple times with same entity produces same result
- **Safe**: Collision checks prevent data loss
- **Efficient**: Single query to check existence, then insert or update
- **Cache-aware**: Clears old cache entries when slugs change
### Examples
#### Update existing endpoint
```typescript
// First call - creates endpoint
await longurl.manageUrl('product', 'laptop-123', 'https://shop.com/laptop');
// Second call - updates endpoint (same entity)
await longurl.manageUrl('product', 'laptop-123', 'https://shop.com/laptop-v2');
// ✅ Updates url_base, merges metadata, preserves created_at
```
#### Update with new URL pattern
```typescript
// Change URL pattern for existing entity
await longurl.manageUrl('product', 'laptop-123', 'https://shop.com/laptop', {}, {
urlPattern: 'new-pattern-{publicId}'
});
// ✅ Updates url_slug, invalidates old slug, preserves entity data
```
#### Metadata merging
```typescript
// First call
await longurl.manageUrl('product', 'laptop-123', 'https://...', {
campaign: 'launch',
source: 'email'
});
// Update call
await longurl.manageUrl('product', 'laptop-123', 'https://...', {
version: '2.0',
updated: true
});
// ✅ Final metadata: { campaign: 'launch', source: 'email', version: '2.0', updated: true }
```
## [0.4.0] - 2025-11-10
### Added
- **NEW**: `url_slug_short` database column for efficient storage
- `url_slug_short` now stored as a column in the same row as `url_slug`
- Eliminates duplicate rows - both slugs stored together
- Added `migration-add-url-slug-short.sql` for easy database updates
- Unique index on `url_slug_short` for fast lookups
### Changed
- **IMPROVED**: Storage architecture for `url_slug_short`
- Previously: `url_slug_short` stored as separate row
- Now: Both `url_slug` and `url_slug_short` in same row
- More efficient: Single row lookup for both URLs
- Better data integrity: Both slugs always point to same `url_base`
- **ENHANCED**: Resolver now handles both slug types
- Resolves by `url_slug` (readable slug)
- Resolves by `url_slug_short` (short Base62 ID)
- Both resolve to the same `url_base` destination
- Automatic fallback: tries `url_slug` first, then `url_slug_short`
- **ENHANCED**: Collision detection checks both columns
- Prevents collisions in both `url_slug` and `url_slug_short`
- Ensures uniqueness across both slug types
- Works with legacy and new schema detection
### Migration
- **REQUIRED**: Run `migration-add-url-slug-short.sql` to add column
- Adds `url_slug_short TEXT UNIQUE` column to `endpoints` table
- Creates index for fast lookups
- Backward compatible: Existing URLs work without migration
- New URLs will populate `url_slug_short` automatically
### Examples
#### Framework Mode with url_slug_short in same row
```typescript
const result = await longurl.manageUrl('product', 'laptop-dell-xps-13', 'https://...', {}, {
enableShortening: false
});
// url_slug: 'laptop-dell-xps-13' (readable)
// url_slug_short: 'X7gT5p' (short for sharing)
// Both stored in same database row
// Both resolve to same url_base
```
#### Resolving by either slug
```typescript
// Both of these resolve to the same destination:
await longurl.resolve('laptop-dell-xps-13'); // url_slug
await longurl.resolve('X7gT5p'); // url_slug_short
```
## [0.3.8] - 2025-07-29
### Added
- **NEW**: `url_slug_short` field for built-in short URLs in Framework Mode
- Framework Mode now automatically generates both readable and short URLs
- `url_slug_short` provides short random IDs for easy sharing (social media, SMS, etc.)
- Both URLs redirect to the same `url_base` destination
- Stored in database for proper routing and analytics
- Maintains naming convention with `url_slug` prefix
### Changed
- **ENHANCED**: Framework Mode now includes built-in Shortening Mode functionality
- Framework Mode = SEO-friendly URLs + built-in short URLs
- `publicId` preserves business context (entity identifiers, campaign names)
- `url_slug_short` provides consistent short format for sharing
- Both URLs get full analytics and tracking
- Perfect for scenarios requiring both SEO and sharing optimization
### Examples
#### Framework Mode with built-in short URL
```typescript
const result = await longurl.manageUrl('product', 'laptop-dell-xps-13', 'https://...', {}, {
enableShortening: false
});
// URL: https://yourdomain.co/laptop-dell-xps-13 (readable, SEO-friendly)
// publicId: 'laptop-dell-xps-13' (business identifier)
// url_slug_short: '5jGX9H' (short for sharing)
// Both redirect to same destination
```
#### Pattern URLs with built-in short URL
```typescript
const result = await longurl.manageUrl('product', 'laptop-123', '/hub/earthfare-organic-bananas-{publicId}', {}, {
urlPattern: 'earthfare-organic-bananas-{publicId}',
includeInSlug: false
});
// URL: https://yourdomain.co/earthfare-organic-bananas (clean)
// publicId: 'yC66VW' (pattern identifier)
// url_slug_short: 'fjetMj' (short for sharing)
// Both redirect to same destination
```
## [0.3.7] - 2025-07-29
### Fixed
- **FIXED**: `includeInSlug: false` now works correctly with pattern URLs
- Pattern URLs now properly respect `includeInSlug: false` setting
- Removes trailing dash + placeholder as a unit for clean URLs
- Preserves publicId for storage while creating clean URL slugs
- Works consistently across all code paths (collision checking, error handling)
- **FIXED**: Pattern resolution in `url_base` storage
- `{publicId}` placeholders now properly resolved before database storage
- Prevents orphaned URLs with unresolved placeholders
- Ensures all stored `url_base` values are fully resolved
- **FIXED**: Parameter passing to pattern generator
- `includeInSlug` parameter now correctly passed to pattern generator
- `generate_qr_code` parameter now correctly passed to pattern generator
- All options properly reach the pattern generation logic
### Changed
- **IMPROVED**: Pattern URL behavior with `includeInSlug: false`
- Clean URLs without trailing dashes when `includeInSlug: false`
- Elegant removal of trailing dash + placeholder as a unit
- User-friendly pattern design (no special rules needed)
- Consistent behavior across all URL generation modes
### Examples
#### Pattern URLs with `includeInSlug: false`
```typescript
const result = await longurl.manageUrl('product', 'laptop-123', '/hub/earthfare-organic-bananas-{publicId}', {}, {
urlPattern: 'earthfare-organic-bananas-{publicId}',
includeInSlug: false
});
// URL: https://yourdomain.co/earthfare-organic-bananas (clean, no trailing dash)
// publicId: 'X7gT5p' (preserved for storage)
// urlBase: /hub/earthfare-organic-bananas-X7gT5p (resolved for routing)
```
## [0.3.6] - 2025-07-29
### Added
- **NEW**: QR code generation for all URLs
- Automatic QR code generation for every URL created
- QR codes returned as base64 data URLs in API response
- Stored in database as `qr_code` column (nullable)
- Optimized for storage (~1.7KB per QR code)
- Can be disabled with `generate_qr_code: false` option
- **NEW**: QR code support in all URL generation modes
- Works in Shortening Mode and Framework Mode
- Works with pattern URLs and custom public IDs
- Graceful error handling if QR generation fails
- **NEW**: QR code utilities and validation
- `generateOptimizedQRCode()` function for efficient QR generation
- `isValidQRCodeDataUrl()` function for validation
- Configurable QR code options (size, error correction, colors)
### Changed
- **UPDATED**: Database schema to include QR code storage
- Added `qr_code` column to `endpoints` table
- QR codes stored as base64 strings for immediate use
- Backward compatible with existing installations
- **ENHANCED**: API response to include QR codes
- `GenerationResult` now includes `qrCode` field
- QR codes available immediately in response
- No additional API calls needed for QR code access
### Performance
- **OPTIMIZED**: QR code generation for production use
- Asynchronous generation to avoid blocking
- Error handling prevents QR failures from breaking URL generation
- Configurable to disable for performance-critical applications
### Examples
#### Basic QR Code Generation
```typescript
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...');
// result.qrCode: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
```
#### Disable QR Code Generation
```typescript
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...', {}, {
generate_qr_code: false
});
// result.qrCode: undefined
```
#### Framework Mode with QR Code
```typescript
const result = await longurl.manageUrl('product', 'laptop-dell-xps-13', 'https://...', {}, {
enableShortening: false,
generate_qr_code: true
});
// URL: https://yourdomain.co/laptop-dell-xps-13
// QR Code: Generated for the readable URL
```
## [0.3.5] - 2025-07-29
### Added
- **NEW**: `includeInSlug` parameter for public ID management
- Added `includeInSlug` option to control whether public IDs appear in URLs
- Works in Framework Mode to separate entity identifiers from URL slugs
- Allows opaque URLs while preserving business identifiers
- Defaults to `true` for backward compatibility
- **NEW**: Enhanced public ID handling in Framework Mode
- `publicId` field always returns the meaningful entity identifier
- `urlId` can be random slug when `includeInSlug: false`
- Perfect for privacy/security scenarios without losing business context
### Changed
- **IMPROVED**: Framework Mode behavior with `includeInSlug` option
- When `includeInSlug: true` (default): Entity ID appears in URL
- When `includeInSlug: false`: Random slug in URL, entity ID preserved in `publicId`
- Shortening Mode ignores `includeInSlug` parameter (as intended)
- **CLARIFIED**: Distinction between Shortening Mode and Framework Mode
- Shortening Mode: Random ID serves as both URL slug and public identifier
- Framework Mode: Entity ID can be separated from URL slug via `includeInSlug`
### Examples
#### Framework Mode with `includeInSlug: true` (Default)
```typescript
const result = await longurl.manageUrl('product', 'laptop-dell-xps-13', 'https://...', {}, {
enableShortening: false,
includeInSlug: true
});
// Result: https://yourdomain.co/laptop-dell-xps-13
// publicId: 'laptop-dell-xps-13' (same as urlId)
```
#### Framework Mode with `includeInSlug: false`
```typescript
const result = await longurl.manageUrl('product', 'laptop-dell-xps-13', 'https://...', {}, {
enableShortening: false,
includeInSlug: false
});
// Result: https://yourdomain.co/X7gT5p
// publicId: 'laptop-dell-xps-13' (preserved separately)
```
#### Shortening Mode (ignores `includeInSlug`)
```typescript
const result = await longurl.manageUrl('campaign', 'summer-sale', 'https://...');
// Result: https://yourdomain.co/X7gT5p
// publicId: 'X7gT5p' (same as urlId)
```
## [0.3.3] - 2025-07-29
### Added
- **NEW**: `publicId` parameter for clearer naming in URL generation
- Added `publicId` parameter to `manageUrl()` and `shorten()` methods
- Added `publicId` parameter to `generateUrlId()` and `generatePatternUrl()` functions
- Added `{publicId}` placeholder support in URL patterns
- Created `UrlGenerationOptions` interface for better type safety
- **NEW**: `publicId` field in response objects
- `GenerationResult` now includes `publicId` field for direct access
- Eliminates need to parse URLs to extract generated public IDs
- Works for both developer-provided and auto-generated public IDs
### Changed
- **IMPROVED**: Clearer distinction between public URL identifiers and database columns
- `publicId` = 6-char string for URLs (what users see)
- `endpoint_id` = UUID database primary key (internal)
- Eliminates confusion between parameter and database column names
- **FIXED**: Deprecated field warnings in `enhanceGenerationResult()`
- Now properly uses new field names (`urlSlug`, `urlBase`, `urlOutput`)
- Maintains backward compatibility with deprecated fields
### Deprecated
- **DEPRECATED**: `endpointId` parameter (will be removed in future major version)
- `endpointId` parameter still works for backward compatibility
- `{endpointId}` placeholder still works for backward compatibility
- Developers encouraged to migrate to `publicId` for clearer naming
### Backward Compatibility
- **FULLY COMPATIBLE**: All existing code continues to work unchanged
- Both `publicId` and `endpointId` parameters work simultaneously
- Both `{publicId}` and `{endpointId}` placeholders work simultaneously
- Internal logic prioritizes `publicId` over `endpointId` when both provided
- No breaking changes for existing implementations
### Documentation
- **ADDED**: Comprehensive `DEPRECATION-NOTICE.md` with migration guide
- **UPDATED**: README.md with new `publicId` examples and usage patterns
- **ADDED**: Test files demonstrating both old and new functionality
- **UPDATED**: TypeScript types for better developer experience
### Examples
#### New Recommended Usage
```typescript
// ✅ RECOMMENDED: Use publicId for clearer naming
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...', {}, {
publicId: 'LAPTOP2024'
});
// ✅ RECOMMENDED: Use {publicId} in patterns
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...', {}, {
urlPattern: 'furniture-vintage-table-lamp-{publicId}'
});
```
#### Backward Compatible Usage
```typescript
// ✅ STILL WORKS: endpointId continues to work
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...', {}, {
endpointId: 'LAPTOP2024'
});
// ✅ STILL WORKS: {endpointId} in patterns continues to work
const result = await longurl.manageUrl('product', 'laptop-123', 'https://...', {}, {
urlPattern: 'furniture-vintage-table-lamp-{endpointId}'
});
```
### Migration Timeline
- **Version 0.3.1 (Current)**: `publicId` added, `endpointId` still supported
- **Future Version**: `endpointId` deprecated with warnings
- **Future Major Version**: `endpointId` removed
## [0.3.0] - 2024-12-18
### Added
- Framework mode for SEO-friendly URL management
- Pattern URL generation with placeholders
- Entity-driven URL organization
- Supabase adapter with backward compatibility
- Comprehensive TypeScript support
### Changed
- Renamed primary table from `short_urls` to `endpoints`
- Updated column names for clearer semantics:
- `url_id` → `url_slug`
- `original_url` → `url_base`
- Enhanced field naming with both new and legacy support
### Deprecated
- `shorten()` method (use `manageUrl()` instead)
- Legacy field names (use `urlSlug`, `urlBase`, `urlOutput`)
## [0.2.0] - 2024-12-17
### Added
- Basic URL shortening functionality
- Supabase integration
- Collision detection
- Analytics tracking
## [0.1.0] - 2024-12-16
### Added
- Initial release
- Core URL shortening capabilities
- Basic TypeScript support