@linkforty/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +459 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/database.d.ts +11 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +118 -0
- package/dist/lib/database.js.map +1 -0
- package/dist/lib/utils.d.ts +26 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +119 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/routes/analytics.d.ts +3 -0
- package/dist/routes/analytics.d.ts.map +1 -0
- package/dist/routes/analytics.js +171 -0
- package/dist/routes/analytics.js.map +1 -0
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +10 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/links.d.ts +3 -0
- package/dist/routes/links.d.ts.map +1 -0
- package/dist/routes/links.js +179 -0
- package/dist/routes/links.js.map +1 -0
- package/dist/routes/redirect.d.ts +3 -0
- package/dist/routes/redirect.d.ts.map +1 -0
- package/dist/routes/redirect.js +138 -0
- package/dist/routes/redirect.js.map +1 -0
- package/dist/scripts/migrate.d.ts +2 -0
- package/dist/scripts/migrate.d.ts.map +1 -0
- package/dist/scripts/migrate.js +17 -0
- package/dist/scripts/migrate.js.map +1 -0
- package/dist/types/index.d.ts +139 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.linkRoutes = linkRoutes;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const database_js_1 = require("../lib/database.js");
|
|
6
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
7
|
+
const createLinkSchema = zod_1.z.object({
|
|
8
|
+
userId: zod_1.z.string().uuid(),
|
|
9
|
+
originalUrl: zod_1.z.string().url(),
|
|
10
|
+
title: zod_1.z.string().optional(),
|
|
11
|
+
iosUrl: zod_1.z.string().url().optional(),
|
|
12
|
+
androidUrl: zod_1.z.string().url().optional(),
|
|
13
|
+
webFallbackUrl: zod_1.z.string().url().optional(),
|
|
14
|
+
customCode: zod_1.z.string().optional(),
|
|
15
|
+
utmParameters: zod_1.z.object({
|
|
16
|
+
source: zod_1.z.string().optional(),
|
|
17
|
+
medium: zod_1.z.string().optional(),
|
|
18
|
+
campaign: zod_1.z.string().optional(),
|
|
19
|
+
term: zod_1.z.string().optional(),
|
|
20
|
+
content: zod_1.z.string().optional(),
|
|
21
|
+
}).optional(),
|
|
22
|
+
targetingRules: zod_1.z.object({
|
|
23
|
+
countries: zod_1.z.array(zod_1.z.string()).optional(),
|
|
24
|
+
devices: zod_1.z.array(zod_1.z.enum(['ios', 'android', 'web'])).optional(),
|
|
25
|
+
languages: zod_1.z.array(zod_1.z.string()).optional(),
|
|
26
|
+
}).optional(),
|
|
27
|
+
expiresAt: zod_1.z.string().datetime().optional(),
|
|
28
|
+
});
|
|
29
|
+
const updateLinkSchema = createLinkSchema.partial().extend({
|
|
30
|
+
isActive: zod_1.z.boolean().optional(),
|
|
31
|
+
}).omit({ userId: true });
|
|
32
|
+
async function linkRoutes(fastify) {
|
|
33
|
+
// Get all links for a user
|
|
34
|
+
fastify.get('/api/links', async (request) => {
|
|
35
|
+
const { userId } = request.query;
|
|
36
|
+
if (!userId) {
|
|
37
|
+
throw new Error('userId query parameter is required');
|
|
38
|
+
}
|
|
39
|
+
const query = `
|
|
40
|
+
SELECT l.*,
|
|
41
|
+
COUNT(ce.id) as click_count
|
|
42
|
+
FROM links l
|
|
43
|
+
LEFT JOIN click_events ce ON l.id = ce.link_id
|
|
44
|
+
WHERE l.user_id = $1
|
|
45
|
+
GROUP BY l.id
|
|
46
|
+
ORDER BY l.created_at DESC
|
|
47
|
+
`;
|
|
48
|
+
const result = await database_js_1.db.query(query, [userId]);
|
|
49
|
+
return result.rows.map(row => ({
|
|
50
|
+
...row,
|
|
51
|
+
clickCount: parseInt(row.click_count),
|
|
52
|
+
utmParameters: row.utm_parameters,
|
|
53
|
+
targetingRules: row.targeting_rules,
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
// Get single link
|
|
57
|
+
fastify.get('/api/links/:id', async (request) => {
|
|
58
|
+
const { id } = request.params;
|
|
59
|
+
const { userId } = request.query;
|
|
60
|
+
if (!userId) {
|
|
61
|
+
throw new Error('userId query parameter is required');
|
|
62
|
+
}
|
|
63
|
+
const result = await database_js_1.db.query(`SELECT l.*, COUNT(ce.id) as click_count
|
|
64
|
+
FROM links l
|
|
65
|
+
LEFT JOIN click_events ce ON l.id = ce.link_id
|
|
66
|
+
WHERE l.id = $1 AND l.user_id = $2
|
|
67
|
+
GROUP BY l.id`, [id, userId]);
|
|
68
|
+
if (result.rows.length === 0) {
|
|
69
|
+
throw new Error('Link not found');
|
|
70
|
+
}
|
|
71
|
+
const link = result.rows[0];
|
|
72
|
+
return {
|
|
73
|
+
...link,
|
|
74
|
+
clickCount: parseInt(link.click_count),
|
|
75
|
+
utmParameters: link.utm_parameters,
|
|
76
|
+
targetingRules: link.targeting_rules,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
// Create link
|
|
80
|
+
fastify.post('/api/links', async (request) => {
|
|
81
|
+
const data = createLinkSchema.parse(request.body);
|
|
82
|
+
// Generate short code
|
|
83
|
+
let shortCode = data.customCode || (0, utils_js_1.generateShortCode)();
|
|
84
|
+
// Ensure short code is unique
|
|
85
|
+
let attempts = 0;
|
|
86
|
+
while (attempts < 10) {
|
|
87
|
+
const existing = await database_js_1.db.query('SELECT id FROM links WHERE short_code = $1', [shortCode]);
|
|
88
|
+
if (existing.rows.length === 0) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
shortCode = (0, utils_js_1.generateShortCode)();
|
|
92
|
+
attempts++;
|
|
93
|
+
}
|
|
94
|
+
if (attempts >= 10) {
|
|
95
|
+
throw new Error('Unable to generate unique short code');
|
|
96
|
+
}
|
|
97
|
+
const result = await database_js_1.db.query(`INSERT INTO links (
|
|
98
|
+
user_id, short_code, original_url, title,
|
|
99
|
+
ios_url, android_url, web_fallback_url, utm_parameters, targeting_rules, expires_at
|
|
100
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
101
|
+
RETURNING *`, [
|
|
102
|
+
data.userId,
|
|
103
|
+
shortCode,
|
|
104
|
+
data.originalUrl,
|
|
105
|
+
data.title || null,
|
|
106
|
+
data.iosUrl || null,
|
|
107
|
+
data.androidUrl || null,
|
|
108
|
+
data.webFallbackUrl || null,
|
|
109
|
+
JSON.stringify(data.utmParameters || {}),
|
|
110
|
+
JSON.stringify(data.targetingRules || {}),
|
|
111
|
+
data.expiresAt || null,
|
|
112
|
+
]);
|
|
113
|
+
const link = result.rows[0];
|
|
114
|
+
return {
|
|
115
|
+
...link,
|
|
116
|
+
clickCount: 0,
|
|
117
|
+
utmParameters: link.utm_parameters,
|
|
118
|
+
targetingRules: link.targeting_rules,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
// Update link
|
|
122
|
+
fastify.put('/api/links/:id', async (request) => {
|
|
123
|
+
const { id } = request.params;
|
|
124
|
+
const { userId } = request.query;
|
|
125
|
+
if (!userId) {
|
|
126
|
+
throw new Error('userId query parameter is required');
|
|
127
|
+
}
|
|
128
|
+
const data = updateLinkSchema.parse(request.body);
|
|
129
|
+
// Build update query dynamically
|
|
130
|
+
const updates = [];
|
|
131
|
+
const values = [];
|
|
132
|
+
let paramIndex = 1;
|
|
133
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
134
|
+
if (value !== undefined) {
|
|
135
|
+
if (key === 'utmParameters' || key === 'targetingRules') {
|
|
136
|
+
updates.push(`${key.replace(/([A-Z])/g, '_$1').toLowerCase()} = $${paramIndex}`);
|
|
137
|
+
values.push(JSON.stringify(value));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const dbKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
141
|
+
updates.push(`${dbKey} = $${paramIndex}`);
|
|
142
|
+
values.push(value);
|
|
143
|
+
}
|
|
144
|
+
paramIndex++;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (updates.length === 0) {
|
|
148
|
+
throw new Error('No updates provided');
|
|
149
|
+
}
|
|
150
|
+
updates.push('updated_at = NOW()');
|
|
151
|
+
values.push(id, userId);
|
|
152
|
+
const result = await database_js_1.db.query(`UPDATE links SET ${updates.join(', ')}
|
|
153
|
+
WHERE id = $${paramIndex} AND user_id = $${paramIndex + 1}
|
|
154
|
+
RETURNING *`, values);
|
|
155
|
+
if (result.rows.length === 0) {
|
|
156
|
+
throw new Error('Link not found');
|
|
157
|
+
}
|
|
158
|
+
const link = result.rows[0];
|
|
159
|
+
return {
|
|
160
|
+
...link,
|
|
161
|
+
utmParameters: link.utm_parameters,
|
|
162
|
+
targetingRules: link.targeting_rules,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
// Delete link
|
|
166
|
+
fastify.delete('/api/links/:id', async (request) => {
|
|
167
|
+
const { id } = request.params;
|
|
168
|
+
const { userId } = request.query;
|
|
169
|
+
if (!userId) {
|
|
170
|
+
throw new Error('userId query parameter is required');
|
|
171
|
+
}
|
|
172
|
+
const result = await database_js_1.db.query('DELETE FROM links WHERE id = $1 AND user_id = $2 RETURNING id', [id, userId]);
|
|
173
|
+
if (result.rows.length === 0) {
|
|
174
|
+
throw new Error('Link not found');
|
|
175
|
+
}
|
|
176
|
+
return { success: true };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/routes/links.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,iBA4J5D"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redirectRoutes = redirectRoutes;
|
|
4
|
+
const database_js_1 = require("../lib/database.js");
|
|
5
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
6
|
+
async function redirectRoutes(fastify) {
|
|
7
|
+
// Handle short link redirects
|
|
8
|
+
fastify.get('/:shortCode', async (request, reply) => {
|
|
9
|
+
const { shortCode } = request.params;
|
|
10
|
+
let linkData = null;
|
|
11
|
+
// Try to get link from cache if Redis is available
|
|
12
|
+
const cacheKey = `link:${shortCode}`;
|
|
13
|
+
if (fastify.redis) {
|
|
14
|
+
try {
|
|
15
|
+
linkData = await fastify.redis.get(cacheKey);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
fastify.log.warn('Redis cache lookup failed, falling back to database');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!linkData) {
|
|
22
|
+
// Get from database
|
|
23
|
+
const result = await database_js_1.db.query(`SELECT * FROM links
|
|
24
|
+
WHERE short_code = $1 AND is_active = true
|
|
25
|
+
AND (expires_at IS NULL OR expires_at > NOW())`, [shortCode]);
|
|
26
|
+
if (result.rows.length === 0) {
|
|
27
|
+
return reply.status(404).send({ error: 'Link not found' });
|
|
28
|
+
}
|
|
29
|
+
linkData = JSON.stringify(result.rows[0]);
|
|
30
|
+
// Cache for 5 minutes if Redis is available
|
|
31
|
+
if (fastify.redis) {
|
|
32
|
+
try {
|
|
33
|
+
await fastify.redis.setex(cacheKey, 300, linkData);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
fastify.log.warn('Redis cache set failed');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const link = JSON.parse(linkData);
|
|
41
|
+
// Check targeting rules BEFORE redirecting
|
|
42
|
+
if (link.targeting_rules) {
|
|
43
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
44
|
+
const ip = request.ip;
|
|
45
|
+
const acceptLanguage = request.headers['accept-language'] || '';
|
|
46
|
+
// Get user's actual data for targeting checks
|
|
47
|
+
const device = (0, utils_js_1.detectDevice)(userAgent);
|
|
48
|
+
const { countryCode } = (0, utils_js_1.getLocationFromIP)(ip);
|
|
49
|
+
// Extract primary language from accept-language header (e.g., "en-US,en;q=0.9" -> "en")
|
|
50
|
+
const primaryLanguage = acceptLanguage.split(',')[0]?.split('-')[0]?.toLowerCase();
|
|
51
|
+
const rules = link.targeting_rules;
|
|
52
|
+
let isTargeted = true;
|
|
53
|
+
// Check country targeting
|
|
54
|
+
if (rules.countries && rules.countries.length > 0) {
|
|
55
|
+
const targetCountries = rules.countries.map((c) => c.toUpperCase());
|
|
56
|
+
if (!countryCode || !targetCountries.includes(countryCode.toUpperCase())) {
|
|
57
|
+
isTargeted = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Check device targeting
|
|
61
|
+
if (rules.devices && rules.devices.length > 0) {
|
|
62
|
+
if (!rules.devices.includes(device)) {
|
|
63
|
+
isTargeted = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Check language targeting
|
|
67
|
+
if (rules.languages && rules.languages.length > 0) {
|
|
68
|
+
const targetLanguages = rules.languages.map((l) => l.toLowerCase());
|
|
69
|
+
if (!primaryLanguage || !targetLanguages.includes(primaryLanguage)) {
|
|
70
|
+
isTargeted = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// If targeting rules exist but user doesn't match, return 404
|
|
74
|
+
if (!isTargeted) {
|
|
75
|
+
return reply.status(404).send({ error: 'Link not found' });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Track click asynchronously
|
|
79
|
+
setImmediate(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
82
|
+
const ip = request.ip;
|
|
83
|
+
const referrer = request.headers.referer || null;
|
|
84
|
+
const { deviceType, platform } = (0, utils_js_1.parseUserAgent)(userAgent);
|
|
85
|
+
const { countryCode, countryName, region, city, latitude, longitude, timezone } = (0, utils_js_1.getLocationFromIP)(ip);
|
|
86
|
+
// Extract UTM parameters from query string
|
|
87
|
+
const query = request.query;
|
|
88
|
+
const utmSource = query?.utm_source;
|
|
89
|
+
const utmMedium = query?.utm_medium;
|
|
90
|
+
const utmCampaign = query?.utm_campaign;
|
|
91
|
+
await database_js_1.db.query(`INSERT INTO click_events (
|
|
92
|
+
link_id, ip_address, user_agent, device_type, platform,
|
|
93
|
+
country_code, country_name, region, city, latitude, longitude, timezone,
|
|
94
|
+
utm_source, utm_medium, utm_campaign, referrer
|
|
95
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`, [
|
|
96
|
+
link.id,
|
|
97
|
+
ip,
|
|
98
|
+
userAgent,
|
|
99
|
+
deviceType,
|
|
100
|
+
platform,
|
|
101
|
+
countryCode,
|
|
102
|
+
countryName,
|
|
103
|
+
region,
|
|
104
|
+
city,
|
|
105
|
+
latitude,
|
|
106
|
+
longitude,
|
|
107
|
+
timezone,
|
|
108
|
+
utmSource,
|
|
109
|
+
utmMedium,
|
|
110
|
+
utmCampaign,
|
|
111
|
+
referrer,
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
fastify.log.error(`Error tracking click: ${error}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Determine redirect URL based on device
|
|
119
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
120
|
+
const device = (0, utils_js_1.detectDevice)(userAgent);
|
|
121
|
+
let redirectUrl = link.original_url;
|
|
122
|
+
// Check for device-specific URLs
|
|
123
|
+
if (device === 'ios' && link.ios_url) {
|
|
124
|
+
redirectUrl = link.ios_url;
|
|
125
|
+
}
|
|
126
|
+
else if (device === 'android' && link.android_url) {
|
|
127
|
+
redirectUrl = link.android_url;
|
|
128
|
+
}
|
|
129
|
+
else if (link.web_fallback_url && device === 'web') {
|
|
130
|
+
redirectUrl = link.web_fallback_url;
|
|
131
|
+
}
|
|
132
|
+
// Add UTM parameters
|
|
133
|
+
const finalUrl = (0, utils_js_1.buildRedirectUrl)(redirectUrl, link.utm_parameters);
|
|
134
|
+
// Redirect
|
|
135
|
+
return reply.redirect(302, finalUrl);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/scripts/migrate.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const database_js_1 = require("../lib/database.js");
|
|
4
|
+
async function runMigrations() {
|
|
5
|
+
try {
|
|
6
|
+
console.log('Starting database migration...');
|
|
7
|
+
await (0, database_js_1.initializeDatabase)();
|
|
8
|
+
console.log('Database migration completed successfully!');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error('Migration failed:', error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
runMigrations();
|
|
17
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/scripts/migrate.ts"],"names":[],"mappings":";;AAAA,oDAAwD;AAExD,KAAK,UAAU,aAAa;IAC1B,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,MAAM,IAAA,gCAAkB,GAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Link {
|
|
9
|
+
id: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
short_code: string;
|
|
12
|
+
original_url: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
ios_url?: string;
|
|
15
|
+
android_url?: string;
|
|
16
|
+
web_fallback_url?: string;
|
|
17
|
+
utmParameters?: UTMParameters;
|
|
18
|
+
targeting_rules?: TargetingRules;
|
|
19
|
+
is_active: boolean;
|
|
20
|
+
expires_at?: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
click_count?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface UTMParameters {
|
|
26
|
+
source?: string;
|
|
27
|
+
medium?: string;
|
|
28
|
+
campaign?: string;
|
|
29
|
+
term?: string;
|
|
30
|
+
content?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TargetingRules {
|
|
33
|
+
countries?: string[];
|
|
34
|
+
devices?: ('ios' | 'android' | 'web')[];
|
|
35
|
+
languages?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface ClickEvent {
|
|
38
|
+
id: string;
|
|
39
|
+
linkId: string;
|
|
40
|
+
clickedAt: string;
|
|
41
|
+
ipAddress?: string;
|
|
42
|
+
userAgent?: string;
|
|
43
|
+
deviceType?: string;
|
|
44
|
+
platform?: string;
|
|
45
|
+
countryCode?: string;
|
|
46
|
+
countryName?: string;
|
|
47
|
+
region?: string;
|
|
48
|
+
city?: string;
|
|
49
|
+
latitude?: number;
|
|
50
|
+
longitude?: number;
|
|
51
|
+
timezone?: string;
|
|
52
|
+
utmSource?: string;
|
|
53
|
+
utmMedium?: string;
|
|
54
|
+
utmCampaign?: string;
|
|
55
|
+
referrer?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CreateLinkRequest {
|
|
58
|
+
originalUrl: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
iosUrl?: string;
|
|
61
|
+
androidUrl?: string;
|
|
62
|
+
webFallbackUrl?: string;
|
|
63
|
+
utmParameters?: UTMParameters;
|
|
64
|
+
targetingRules?: TargetingRules;
|
|
65
|
+
customCode?: string;
|
|
66
|
+
expiresAt?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface UpdateLinkRequest extends Partial<CreateLinkRequest> {
|
|
69
|
+
isActive?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface AnalyticsData {
|
|
72
|
+
totalClicks: number;
|
|
73
|
+
uniqueClicks: number;
|
|
74
|
+
clicksByDate: Array<{
|
|
75
|
+
date: string;
|
|
76
|
+
clicks: number;
|
|
77
|
+
}>;
|
|
78
|
+
clicksByCountry: Array<{
|
|
79
|
+
country: string;
|
|
80
|
+
countryCode: string;
|
|
81
|
+
clicks: number;
|
|
82
|
+
}>;
|
|
83
|
+
clicksByCity: Array<{
|
|
84
|
+
city: string;
|
|
85
|
+
countryCode: string;
|
|
86
|
+
region: string;
|
|
87
|
+
clicks: number;
|
|
88
|
+
}>;
|
|
89
|
+
clicksByRegion: Array<{
|
|
90
|
+
region: string;
|
|
91
|
+
countryCode: string;
|
|
92
|
+
clicks: number;
|
|
93
|
+
}>;
|
|
94
|
+
clicksByTimezone: Array<{
|
|
95
|
+
timezone: string;
|
|
96
|
+
clicks: number;
|
|
97
|
+
}>;
|
|
98
|
+
clicksByDevice: Array<{
|
|
99
|
+
device: string;
|
|
100
|
+
clicks: number;
|
|
101
|
+
}>;
|
|
102
|
+
clicksByPlatform: Array<{
|
|
103
|
+
platform: string;
|
|
104
|
+
clicks: number;
|
|
105
|
+
}>;
|
|
106
|
+
clicksByBrowser: Array<{
|
|
107
|
+
browser: string;
|
|
108
|
+
clicks: number;
|
|
109
|
+
}>;
|
|
110
|
+
clicksByHour: Array<{
|
|
111
|
+
hour: number;
|
|
112
|
+
clicks: number;
|
|
113
|
+
}>;
|
|
114
|
+
clicksByUtmSource: Array<{
|
|
115
|
+
source: string;
|
|
116
|
+
clicks: number;
|
|
117
|
+
}>;
|
|
118
|
+
clicksByUtmMedium: Array<{
|
|
119
|
+
medium: string;
|
|
120
|
+
clicks: number;
|
|
121
|
+
}>;
|
|
122
|
+
clicksByUtmCampaign: Array<{
|
|
123
|
+
campaign: string;
|
|
124
|
+
clicks: number;
|
|
125
|
+
}>;
|
|
126
|
+
clicksByReferrer: Array<{
|
|
127
|
+
source: string;
|
|
128
|
+
clicks: number;
|
|
129
|
+
}>;
|
|
130
|
+
topLinks: Array<{
|
|
131
|
+
id: string;
|
|
132
|
+
shortCode: string;
|
|
133
|
+
title: string | null;
|
|
134
|
+
originalUrl: string;
|
|
135
|
+
totalClicks: number;
|
|
136
|
+
uniqueClicks: number;
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linkforty/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Open-source deeplink management engine with device detection and analytics",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/linkforty/core"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"deeplink",
|
|
14
|
+
"onelink",
|
|
15
|
+
"mobile-links",
|
|
16
|
+
"device-detection",
|
|
17
|
+
"url-shortener",
|
|
18
|
+
"analytics"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsx watch src/index.ts",
|
|
23
|
+
"migrate": "tsx src/scripts/migrate.ts",
|
|
24
|
+
"test": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"fastify": "^4.24.0",
|
|
28
|
+
"@fastify/cors": "^8.4.0",
|
|
29
|
+
"@fastify/redis": "^6.1.0",
|
|
30
|
+
"pg": "^8.11.0",
|
|
31
|
+
"nanoid": "^5.0.0",
|
|
32
|
+
"geoip-lite": "^1.4.7",
|
|
33
|
+
"ua-parser-js": "^1.0.37",
|
|
34
|
+
"zod": "^3.22.0",
|
|
35
|
+
"dotenv": "^16.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.8.0",
|
|
39
|
+
"@types/pg": "^8.10.0",
|
|
40
|
+
"@types/geoip-lite": "^1.4.4",
|
|
41
|
+
"@types/ua-parser-js": "^0.7.39",
|
|
42
|
+
"tsx": "^4.0.0",
|
|
43
|
+
"typescript": "^5.2.0",
|
|
44
|
+
"vitest": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"exports": {
|
|
47
|
+
".": "./dist/index.js",
|
|
48
|
+
"./utils": "./dist/lib/utils.js",
|
|
49
|
+
"./database": "./dist/lib/database.js",
|
|
50
|
+
"./routes": "./dist/routes/index.js",
|
|
51
|
+
"./types": "./dist/types/index.js"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"migrations",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
]
|
|
59
|
+
}
|