lsh-framework 3.1.6 → 3.1.7
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/dist/__tests__/fixtures/job-fixtures.js +204 -0
- package/dist/__tests__/fixtures/supabase-mocks.js +252 -0
- package/dist/cli.js +12 -2
- package/dist/lib/database-types.js +90 -0
- package/dist/lib/lsh-error.js +406 -0
- package/dist/lib/saas-auth.js +39 -2
- package/dist/lib/saas-billing.js +106 -8
- package/dist/lib/saas-organizations.js +74 -5
- package/dist/lib/saas-secrets.js +30 -2
- package/package.json +3 -3
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Fixtures for Testing
|
|
3
|
+
*
|
|
4
|
+
* Provides factory functions and sample data for testing job-related functionality.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { createTestJob, SAMPLE_JOBS } from '../fixtures/job-fixtures';
|
|
9
|
+
*
|
|
10
|
+
* const job = createTestJob({ name: 'my-test-job', command: 'echo test' });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// FACTORY FUNCTIONS
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Create a test job specification with sensible defaults.
|
|
18
|
+
*/
|
|
19
|
+
export function createTestJob(overrides = {}) {
|
|
20
|
+
const id = `job_${Math.random().toString(36).substr(2, 9)}`;
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
name: 'test-job',
|
|
24
|
+
command: 'echo "test"',
|
|
25
|
+
args: [],
|
|
26
|
+
status: 'created',
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
tags: [],
|
|
29
|
+
priority: 5,
|
|
30
|
+
maxRetries: 3,
|
|
31
|
+
retryCount: 0,
|
|
32
|
+
databaseSync: false, // Disable DB sync in tests by default
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a test job execution record.
|
|
38
|
+
*/
|
|
39
|
+
export function createTestExecution(overrides = {}) {
|
|
40
|
+
const executionId = `exec_${Math.random().toString(36).substr(2, 9)}`;
|
|
41
|
+
return {
|
|
42
|
+
executionId,
|
|
43
|
+
jobId: `job_${Math.random().toString(36).substr(2, 9)}`,
|
|
44
|
+
jobName: 'test-job',
|
|
45
|
+
command: 'echo "test"',
|
|
46
|
+
startTime: new Date(),
|
|
47
|
+
status: 'completed',
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a scheduled job with cron expression.
|
|
54
|
+
*/
|
|
55
|
+
export function createScheduledJob(cronExpression, overrides = {}) {
|
|
56
|
+
return createTestJob({
|
|
57
|
+
name: 'scheduled-job',
|
|
58
|
+
command: './scripts/scheduled.sh',
|
|
59
|
+
schedule: {
|
|
60
|
+
cron: cronExpression,
|
|
61
|
+
},
|
|
62
|
+
...overrides,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create a job with interval scheduling.
|
|
67
|
+
*/
|
|
68
|
+
export function createIntervalJob(intervalMs, overrides = {}) {
|
|
69
|
+
return createTestJob({
|
|
70
|
+
name: 'interval-job',
|
|
71
|
+
command: './scripts/interval.sh',
|
|
72
|
+
schedule: {
|
|
73
|
+
interval: intervalMs,
|
|
74
|
+
},
|
|
75
|
+
...overrides,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// SAMPLE DATA
|
|
80
|
+
// ============================================================================
|
|
81
|
+
/**
|
|
82
|
+
* Collection of sample jobs for different test scenarios.
|
|
83
|
+
*/
|
|
84
|
+
export const SAMPLE_JOBS = {
|
|
85
|
+
/** Simple echo job */
|
|
86
|
+
simple: createTestJob({
|
|
87
|
+
id: 'job_simple',
|
|
88
|
+
name: 'simple-job',
|
|
89
|
+
command: 'echo "hello"',
|
|
90
|
+
}),
|
|
91
|
+
/** Job with environment variables */
|
|
92
|
+
withEnv: createTestJob({
|
|
93
|
+
id: 'job_with_env',
|
|
94
|
+
name: 'env-job',
|
|
95
|
+
command: 'printenv',
|
|
96
|
+
env: {
|
|
97
|
+
MY_VAR: 'test_value',
|
|
98
|
+
ANOTHER_VAR: '123',
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
/** Job with arguments */
|
|
102
|
+
withArgs: createTestJob({
|
|
103
|
+
id: 'job_with_args',
|
|
104
|
+
name: 'args-job',
|
|
105
|
+
command: 'ls',
|
|
106
|
+
args: ['-la', '/tmp'],
|
|
107
|
+
}),
|
|
108
|
+
/** Scheduled job (daily at midnight) */
|
|
109
|
+
scheduled: createScheduledJob('0 0 * * *', {
|
|
110
|
+
id: 'job_scheduled',
|
|
111
|
+
name: 'daily-job',
|
|
112
|
+
}),
|
|
113
|
+
/** Interval job (every 5 minutes) */
|
|
114
|
+
interval: createIntervalJob(5 * 60 * 1000, {
|
|
115
|
+
id: 'job_interval',
|
|
116
|
+
name: 'frequent-job',
|
|
117
|
+
}),
|
|
118
|
+
/** Long-running job with timeout */
|
|
119
|
+
longRunning: createTestJob({
|
|
120
|
+
id: 'job_long',
|
|
121
|
+
name: 'long-running-job',
|
|
122
|
+
command: 'sleep 300',
|
|
123
|
+
timeout: 60000, // 1 minute timeout
|
|
124
|
+
}),
|
|
125
|
+
/** Job that will fail */
|
|
126
|
+
failing: createTestJob({
|
|
127
|
+
id: 'job_failing',
|
|
128
|
+
name: 'failing-job',
|
|
129
|
+
command: 'exit 1',
|
|
130
|
+
maxRetries: 2,
|
|
131
|
+
}),
|
|
132
|
+
/** Completed job */
|
|
133
|
+
completed: createTestJob({
|
|
134
|
+
id: 'job_completed',
|
|
135
|
+
name: 'completed-job',
|
|
136
|
+
command: 'echo "done"',
|
|
137
|
+
status: 'completed',
|
|
138
|
+
startedAt: new Date(Date.now() - 1000),
|
|
139
|
+
completedAt: new Date(),
|
|
140
|
+
exitCode: 0,
|
|
141
|
+
stdout: 'done\n',
|
|
142
|
+
}),
|
|
143
|
+
/** Failed job */
|
|
144
|
+
failed: createTestJob({
|
|
145
|
+
id: 'job_failed',
|
|
146
|
+
name: 'failed-job',
|
|
147
|
+
command: 'false',
|
|
148
|
+
status: 'failed',
|
|
149
|
+
startedAt: new Date(Date.now() - 1000),
|
|
150
|
+
completedAt: new Date(),
|
|
151
|
+
exitCode: 1,
|
|
152
|
+
stderr: 'Command failed\n',
|
|
153
|
+
retryCount: 3,
|
|
154
|
+
}),
|
|
155
|
+
/** Secrets rotation job (realistic example) */
|
|
156
|
+
secretsRotation: createScheduledJob('0 2 1 * *', {
|
|
157
|
+
id: 'job_secrets_rotation',
|
|
158
|
+
name: 'rotate-api-keys',
|
|
159
|
+
command: './examples/secrets-rotation/rotate-api-keys.sh',
|
|
160
|
+
description: 'Monthly API key rotation',
|
|
161
|
+
tags: ['secrets', 'maintenance', 'security'],
|
|
162
|
+
env: {
|
|
163
|
+
LSH_ENVIRONMENT: 'production',
|
|
164
|
+
LOG_LEVEL: 'info',
|
|
165
|
+
},
|
|
166
|
+
timeout: 300000, // 5 minutes
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Sample execution records for testing history/statistics.
|
|
171
|
+
*/
|
|
172
|
+
export const SAMPLE_EXECUTIONS = {
|
|
173
|
+
success: createTestExecution({
|
|
174
|
+
executionId: 'exec_success',
|
|
175
|
+
jobId: 'job_simple',
|
|
176
|
+
status: 'completed',
|
|
177
|
+
exitCode: 0,
|
|
178
|
+
duration: 150,
|
|
179
|
+
stdout: 'hello\n',
|
|
180
|
+
}),
|
|
181
|
+
failure: createTestExecution({
|
|
182
|
+
executionId: 'exec_failure',
|
|
183
|
+
jobId: 'job_failing',
|
|
184
|
+
status: 'failed',
|
|
185
|
+
exitCode: 1,
|
|
186
|
+
duration: 50,
|
|
187
|
+
stderr: 'Error: Command failed\n',
|
|
188
|
+
errorMessage: 'Process exited with code 1',
|
|
189
|
+
}),
|
|
190
|
+
timeout: createTestExecution({
|
|
191
|
+
executionId: 'exec_timeout',
|
|
192
|
+
jobId: 'job_long',
|
|
193
|
+
status: 'timeout',
|
|
194
|
+
duration: 60000,
|
|
195
|
+
errorMessage: 'Job exceeded timeout of 60000ms',
|
|
196
|
+
}),
|
|
197
|
+
killed: createTestExecution({
|
|
198
|
+
executionId: 'exec_killed',
|
|
199
|
+
jobId: 'job_simple',
|
|
200
|
+
status: 'killed',
|
|
201
|
+
duration: 5000,
|
|
202
|
+
errorMessage: 'Process killed by signal SIGTERM',
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Mock Utilities for Testing
|
|
3
|
+
*
|
|
4
|
+
* Provides mock Supabase client and factory functions for creating
|
|
5
|
+
* test data that matches the database schema.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createMockSupabase, mockOrganization } from '../fixtures/supabase-mocks';
|
|
10
|
+
*
|
|
11
|
+
* const supabase = createMockSupabase({
|
|
12
|
+
* organizations: [mockOrganization({ name: 'Test Org' })],
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// FACTORY FUNCTIONS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Create a mock organization record with sensible defaults.
|
|
21
|
+
* Override any field by passing it in the partial.
|
|
22
|
+
*/
|
|
23
|
+
export function mockOrganization(overrides = {}) {
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
return {
|
|
26
|
+
id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
27
|
+
name: 'Test Organization',
|
|
28
|
+
slug: 'test-org',
|
|
29
|
+
stripe_customer_id: null,
|
|
30
|
+
subscription_tier: 'free',
|
|
31
|
+
subscription_status: 'active',
|
|
32
|
+
subscription_expires_at: null,
|
|
33
|
+
settings: null,
|
|
34
|
+
created_at: now,
|
|
35
|
+
updated_at: now,
|
|
36
|
+
deleted_at: null,
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a mock user record with sensible defaults.
|
|
42
|
+
*/
|
|
43
|
+
export function mockUser(overrides = {}) {
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
const id = `user_${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
email: `${id}@example.com`,
|
|
49
|
+
email_verified: true,
|
|
50
|
+
email_verification_token: null,
|
|
51
|
+
email_verification_expires_at: null,
|
|
52
|
+
password_hash: '$2b$12$mockhashmockhashmockhashmockhash', // Not a real hash
|
|
53
|
+
oauth_provider: null,
|
|
54
|
+
oauth_provider_id: null,
|
|
55
|
+
first_name: 'Test',
|
|
56
|
+
last_name: 'User',
|
|
57
|
+
avatar_url: null,
|
|
58
|
+
last_login_at: now,
|
|
59
|
+
last_login_ip: '127.0.0.1',
|
|
60
|
+
created_at: now,
|
|
61
|
+
updated_at: now,
|
|
62
|
+
deleted_at: null,
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create a mock team record with sensible defaults.
|
|
68
|
+
*/
|
|
69
|
+
export function mockTeam(overrides = {}) {
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
return {
|
|
72
|
+
id: `team_${Math.random().toString(36).substr(2, 9)}`,
|
|
73
|
+
organization_id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
74
|
+
name: 'Test Team',
|
|
75
|
+
slug: 'test-team',
|
|
76
|
+
description: 'A test team',
|
|
77
|
+
encryption_key_id: null,
|
|
78
|
+
created_at: now,
|
|
79
|
+
updated_at: now,
|
|
80
|
+
deleted_at: null,
|
|
81
|
+
...overrides,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a mock secret record with sensible defaults.
|
|
86
|
+
*/
|
|
87
|
+
export function mockSecret(overrides = {}) {
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
return {
|
|
90
|
+
id: `secret_${Math.random().toString(36).substr(2, 9)}`,
|
|
91
|
+
team_id: `team_${Math.random().toString(36).substr(2, 9)}`,
|
|
92
|
+
environment: 'development',
|
|
93
|
+
key: 'TEST_SECRET',
|
|
94
|
+
encrypted_value: 'encrypted_test_value',
|
|
95
|
+
encryption_key_id: `key_${Math.random().toString(36).substr(2, 9)}`,
|
|
96
|
+
description: 'A test secret',
|
|
97
|
+
tags: '[]',
|
|
98
|
+
last_rotated_at: null,
|
|
99
|
+
rotation_interval_days: null,
|
|
100
|
+
created_at: now,
|
|
101
|
+
created_by: null,
|
|
102
|
+
updated_at: now,
|
|
103
|
+
updated_by: null,
|
|
104
|
+
deleted_at: null,
|
|
105
|
+
deleted_by: null,
|
|
106
|
+
...overrides,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a mock organization member record.
|
|
111
|
+
*/
|
|
112
|
+
export function mockOrgMember(overrides = {}) {
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
return {
|
|
115
|
+
id: `member_${Math.random().toString(36).substr(2, 9)}`,
|
|
116
|
+
organization_id: `org_${Math.random().toString(36).substr(2, 9)}`,
|
|
117
|
+
user_id: `user_${Math.random().toString(36).substr(2, 9)}`,
|
|
118
|
+
role: 'member',
|
|
119
|
+
invited_by: null,
|
|
120
|
+
invited_at: now,
|
|
121
|
+
accepted_at: now,
|
|
122
|
+
created_at: now,
|
|
123
|
+
updated_at: now,
|
|
124
|
+
...overrides,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create a successful Supabase response.
|
|
129
|
+
*/
|
|
130
|
+
export function mockSuccess(data) {
|
|
131
|
+
return { data, error: null };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Create an error Supabase response.
|
|
135
|
+
*/
|
|
136
|
+
export function mockError(message, code = 'PGRST000') {
|
|
137
|
+
return { data: null, error: { message, code } };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a mock Supabase client for testing.
|
|
141
|
+
*
|
|
142
|
+
* Returns data from the provided config based on query parameters.
|
|
143
|
+
* Supports basic filtering via .eq() and .single().
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const mockClient = createMockSupabase({
|
|
148
|
+
* organizations: [mockOrganization({ id: 'org_123' })],
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // In test
|
|
152
|
+
* jest.mock('../../lib/supabase-client', () => ({
|
|
153
|
+
* getSupabaseClient: () => mockClient,
|
|
154
|
+
* }));
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createMockSupabase(config = {}) {
|
|
158
|
+
const tables = {
|
|
159
|
+
organizations: config.organizations || [],
|
|
160
|
+
users: config.users || [],
|
|
161
|
+
teams: config.teams || [],
|
|
162
|
+
secrets: config.secrets || [],
|
|
163
|
+
organization_members: config.members || [],
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
from: (tableName) => {
|
|
167
|
+
const tableData = tables[tableName] || [];
|
|
168
|
+
let filteredData = [...tableData];
|
|
169
|
+
let isSingle = false;
|
|
170
|
+
const queryBuilder = {
|
|
171
|
+
select: (_columns) => queryBuilder,
|
|
172
|
+
insert: (data) => {
|
|
173
|
+
if (Array.isArray(data)) {
|
|
174
|
+
tableData.push(...data);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
tableData.push(data);
|
|
178
|
+
}
|
|
179
|
+
filteredData = Array.isArray(data) ? data : [data];
|
|
180
|
+
return queryBuilder;
|
|
181
|
+
},
|
|
182
|
+
update: (_data) => queryBuilder,
|
|
183
|
+
delete: () => queryBuilder,
|
|
184
|
+
eq: (column, value) => {
|
|
185
|
+
filteredData = filteredData.filter((row) => row[column] === value);
|
|
186
|
+
return queryBuilder;
|
|
187
|
+
},
|
|
188
|
+
neq: (column, value) => {
|
|
189
|
+
filteredData = filteredData.filter((row) => row[column] !== value);
|
|
190
|
+
return queryBuilder;
|
|
191
|
+
},
|
|
192
|
+
is: (column, value) => {
|
|
193
|
+
filteredData = filteredData.filter((row) => row[column] === value);
|
|
194
|
+
return queryBuilder;
|
|
195
|
+
},
|
|
196
|
+
order: (_column, _options) => queryBuilder,
|
|
197
|
+
limit: (count) => {
|
|
198
|
+
filteredData = filteredData.slice(0, count);
|
|
199
|
+
return queryBuilder;
|
|
200
|
+
},
|
|
201
|
+
single: () => {
|
|
202
|
+
isSingle = true;
|
|
203
|
+
return queryBuilder;
|
|
204
|
+
},
|
|
205
|
+
then: (resolve) => {
|
|
206
|
+
if (isSingle) {
|
|
207
|
+
if (filteredData.length === 0) {
|
|
208
|
+
resolve(mockError('No rows found', 'PGRST116'));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
resolve(mockSuccess(filteredData[0]));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
resolve(mockSuccess(filteredData));
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
return queryBuilder;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// SAMPLE DATA
|
|
225
|
+
// ============================================================================
|
|
226
|
+
/**
|
|
227
|
+
* Pre-built sample organization for quick testing.
|
|
228
|
+
*/
|
|
229
|
+
export const SAMPLE_ORG = mockOrganization({
|
|
230
|
+
id: 'org_sample123',
|
|
231
|
+
name: 'Sample Organization',
|
|
232
|
+
slug: 'sample-org',
|
|
233
|
+
subscription_tier: 'pro',
|
|
234
|
+
});
|
|
235
|
+
/**
|
|
236
|
+
* Pre-built sample user for quick testing.
|
|
237
|
+
*/
|
|
238
|
+
export const SAMPLE_USER = mockUser({
|
|
239
|
+
id: 'user_sample123',
|
|
240
|
+
email: 'sample@example.com',
|
|
241
|
+
first_name: 'Sample',
|
|
242
|
+
last_name: 'User',
|
|
243
|
+
});
|
|
244
|
+
/**
|
|
245
|
+
* Pre-built sample team for quick testing.
|
|
246
|
+
*/
|
|
247
|
+
export const SAMPLE_TEAM = mockTeam({
|
|
248
|
+
id: 'team_sample123',
|
|
249
|
+
organization_id: 'org_sample123',
|
|
250
|
+
name: 'Sample Team',
|
|
251
|
+
slug: 'sample-team',
|
|
252
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -164,13 +164,23 @@ function findSimilarCommands(input, validCommands) {
|
|
|
164
164
|
const args = process.argv.slice(2);
|
|
165
165
|
if (args.length > 0) {
|
|
166
166
|
const firstArg = args[0];
|
|
167
|
-
|
|
167
|
+
// Include both command names AND their aliases
|
|
168
|
+
const validCommands = [];
|
|
169
|
+
program.commands.forEach(cmd => {
|
|
170
|
+
validCommands.push(cmd.name());
|
|
171
|
+
const aliases = cmd.aliases();
|
|
172
|
+
if (aliases && aliases.length > 0) {
|
|
173
|
+
validCommands.push(...aliases);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
168
176
|
const validOptions = ['-v', '--verbose', '-d', '--debug', '-h', '--help', '-V', '--version'];
|
|
169
177
|
// Check if first argument looks like a command but isn't valid
|
|
170
178
|
if (!firstArg.startsWith('-') &&
|
|
171
179
|
!validCommands.includes(firstArg) &&
|
|
172
180
|
!validOptions.some(opt => args.includes(opt))) {
|
|
173
|
-
|
|
181
|
+
// For suggestions, only use primary command names (not aliases)
|
|
182
|
+
const primaryCommands = program.commands.map(cmd => cmd.name());
|
|
183
|
+
const suggestions = findSimilarCommands(firstArg, primaryCommands);
|
|
174
184
|
console.error(`error: unknown command '${firstArg}'`);
|
|
175
185
|
if (suggestions.length > 0) {
|
|
176
186
|
console.error(`\nDid you mean one of these?`);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH Database Record Types
|
|
3
|
+
*
|
|
4
|
+
* These interfaces represent the exact shape of data returned from Supabase/PostgreSQL.
|
|
5
|
+
* They use snake_case to match database column names, distinguishing them from
|
|
6
|
+
* the domain model types in saas-types.ts which use camelCase.
|
|
7
|
+
*
|
|
8
|
+
* Use these types for:
|
|
9
|
+
* - Typing Supabase query results
|
|
10
|
+
* - Mapping functions that convert DB rows to domain objects
|
|
11
|
+
* - Understanding the database schema without checking migrations
|
|
12
|
+
*
|
|
13
|
+
* @see saas-types.ts for domain model types (camelCase)
|
|
14
|
+
* @see database-schema.ts for schema definitions
|
|
15
|
+
*/
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// TYPE GUARDS & UTILITIES
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Type guard to check if a value is a valid DbOrganizationRecord.
|
|
21
|
+
* Useful for runtime validation of Supabase responses.
|
|
22
|
+
*/
|
|
23
|
+
export function isDbOrganizationRecord(value) {
|
|
24
|
+
if (typeof value !== 'object' || value === null)
|
|
25
|
+
return false;
|
|
26
|
+
const record = value;
|
|
27
|
+
return (typeof record.id === 'string' &&
|
|
28
|
+
typeof record.name === 'string' &&
|
|
29
|
+
typeof record.slug === 'string' &&
|
|
30
|
+
typeof record.created_at === 'string');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Type guard to check if a value is a valid DbUserRecord.
|
|
34
|
+
*/
|
|
35
|
+
export function isDbUserRecord(value) {
|
|
36
|
+
if (typeof value !== 'object' || value === null)
|
|
37
|
+
return false;
|
|
38
|
+
const record = value;
|
|
39
|
+
return (typeof record.id === 'string' &&
|
|
40
|
+
typeof record.email === 'string' &&
|
|
41
|
+
typeof record.created_at === 'string');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Type guard to check if a value is a valid DbSecretRecord.
|
|
45
|
+
*/
|
|
46
|
+
export function isDbSecretRecord(value) {
|
|
47
|
+
if (typeof value !== 'object' || value === null)
|
|
48
|
+
return false;
|
|
49
|
+
const record = value;
|
|
50
|
+
return (typeof record.id === 'string' &&
|
|
51
|
+
typeof record.team_id === 'string' &&
|
|
52
|
+
typeof record.key === 'string' &&
|
|
53
|
+
typeof record.encrypted_value === 'string');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse tags from database - handles both string JSON and array formats.
|
|
57
|
+
*/
|
|
58
|
+
export function parseDbTags(tags) {
|
|
59
|
+
if (!tags)
|
|
60
|
+
return [];
|
|
61
|
+
if (Array.isArray(tags))
|
|
62
|
+
return tags;
|
|
63
|
+
if (typeof tags === 'string') {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(tags);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Safely parse an ISO timestamp string to Date.
|
|
75
|
+
* Returns null for null/undefined input.
|
|
76
|
+
*/
|
|
77
|
+
export function parseDbTimestamp(timestamp) {
|
|
78
|
+
if (!timestamp)
|
|
79
|
+
return null;
|
|
80
|
+
return new Date(timestamp);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Safely parse an ISO timestamp string to Date.
|
|
84
|
+
* Returns current date for null/undefined input.
|
|
85
|
+
*/
|
|
86
|
+
export function parseDbTimestampRequired(timestamp) {
|
|
87
|
+
if (!timestamp)
|
|
88
|
+
return new Date();
|
|
89
|
+
return new Date(timestamp);
|
|
90
|
+
}
|