@opensaas/stack-auth 0.1.0 → 0.1.1

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.
@@ -0,0 +1,356 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ createUserList,
4
+ createSessionList,
5
+ createAccountList,
6
+ createVerificationList,
7
+ getAuthLists,
8
+ } from '../src/lists/index.js'
9
+ import { text } from '@opensaas/stack-core/fields'
10
+
11
+ describe('createUserList', () => {
12
+ it('should create User list with required fields', () => {
13
+ const userList = createUserList()
14
+
15
+ expect(userList.fields).toHaveProperty('name')
16
+ expect(userList.fields).toHaveProperty('email')
17
+ expect(userList.fields).toHaveProperty('emailVerified')
18
+ expect(userList.fields).toHaveProperty('image')
19
+ expect(userList.fields).toHaveProperty('sessions')
20
+ expect(userList.fields).toHaveProperty('accounts')
21
+ })
22
+
23
+ it('should have email field marked as unique', () => {
24
+ const userList = createUserList()
25
+
26
+ expect(userList.fields.email.isIndexed).toBe('unique')
27
+ })
28
+
29
+ it('should have name field marked as required', () => {
30
+ const userList = createUserList()
31
+
32
+ expect(userList.fields.name.validation?.isRequired).toBe(true)
33
+ })
34
+
35
+ it('should have email field marked as required', () => {
36
+ const userList = createUserList()
37
+
38
+ expect(userList.fields.email.validation?.isRequired).toBe(true)
39
+ })
40
+
41
+ it('should have emailVerified field with default false', () => {
42
+ const userList = createUserList()
43
+
44
+ expect(userList.fields.emailVerified.defaultValue).toBe(false)
45
+ })
46
+
47
+ it('should extend User list with custom fields', () => {
48
+ const userList = createUserList({
49
+ fields: {
50
+ role: text(),
51
+ company: text(),
52
+ },
53
+ })
54
+
55
+ expect(userList.fields).toHaveProperty('role')
56
+ expect(userList.fields).toHaveProperty('company')
57
+ // Should also have base fields
58
+ expect(userList.fields).toHaveProperty('email')
59
+ })
60
+
61
+ it('should have default access control', () => {
62
+ const userList = createUserList()
63
+
64
+ expect(userList.access).toBeDefined()
65
+ expect(userList.access?.operation).toBeDefined()
66
+ expect(userList.access?.operation?.query).toBeDefined()
67
+ expect(userList.access?.operation?.create).toBeDefined()
68
+ expect(userList.access?.operation?.update).toBeDefined()
69
+ expect(userList.access?.operation?.delete).toBeDefined()
70
+ })
71
+
72
+ it('should allow querying users', () => {
73
+ const userList = createUserList()
74
+ const queryAccess = userList.access?.operation?.query
75
+
76
+ expect(typeof queryAccess).toBe('function')
77
+ expect(queryAccess?.({})).toBe(true)
78
+ })
79
+
80
+ it('should allow creating users', () => {
81
+ const userList = createUserList()
82
+ const createAccess = userList.access?.operation?.create
83
+
84
+ expect(typeof createAccess).toBe('function')
85
+ expect(createAccess?.({})).toBe(true)
86
+ })
87
+
88
+ it('should allow users to update their own record', () => {
89
+ const userList = createUserList()
90
+ const updateAccess = userList.access?.operation?.update
91
+
92
+ expect(typeof updateAccess).toBe('function')
93
+ expect(
94
+ updateAccess?.({
95
+ session: { userId: 'user-1' },
96
+ item: { id: 'user-1' },
97
+ }),
98
+ ).toBe(true)
99
+ })
100
+
101
+ it('should deny users from updating other users', () => {
102
+ const userList = createUserList()
103
+ const updateAccess = userList.access?.operation?.update
104
+
105
+ expect(
106
+ updateAccess?.({
107
+ session: { userId: 'user-1' },
108
+ item: { id: 'user-2' },
109
+ }),
110
+ ).toBe(false)
111
+ })
112
+
113
+ it('should allow custom access control', () => {
114
+ const customAccess = {
115
+ operation: {
116
+ query: () => false,
117
+ create: () => false,
118
+ update: () => false,
119
+ delete: () => false,
120
+ },
121
+ }
122
+
123
+ const userList = createUserList({
124
+ access: customAccess,
125
+ })
126
+
127
+ expect(userList.access).toEqual(customAccess)
128
+ })
129
+
130
+ it('should support custom hooks', () => {
131
+ const customHooks = {
132
+ resolveInput: async ({ resolvedData }: { resolvedData: unknown }) => resolvedData,
133
+ }
134
+
135
+ const userList = createUserList({
136
+ hooks: customHooks,
137
+ })
138
+
139
+ expect(userList.hooks).toEqual(customHooks)
140
+ })
141
+ })
142
+
143
+ describe('createSessionList', () => {
144
+ it('should create Session list with required fields', () => {
145
+ const sessionList = createSessionList()
146
+
147
+ expect(sessionList.fields).toHaveProperty('token')
148
+ expect(sessionList.fields).toHaveProperty('expiresAt')
149
+ expect(sessionList.fields).toHaveProperty('ipAddress')
150
+ expect(sessionList.fields).toHaveProperty('userAgent')
151
+ expect(sessionList.fields).toHaveProperty('user')
152
+ })
153
+
154
+ it('should have token field marked as unique', () => {
155
+ const sessionList = createSessionList()
156
+
157
+ expect(sessionList.fields.token.isIndexed).toBe('unique')
158
+ })
159
+
160
+ it('should have token field marked as required', () => {
161
+ const sessionList = createSessionList()
162
+
163
+ expect(sessionList.fields.token.validation?.isRequired).toBe(true)
164
+ })
165
+
166
+ it('should have user relationship', () => {
167
+ const sessionList = createSessionList()
168
+
169
+ expect(sessionList.fields.user.type).toBe('relationship')
170
+ expect(sessionList.fields.user.ref).toBe('User.sessions')
171
+ })
172
+
173
+ it('should have restrictive access control', () => {
174
+ const sessionList = createSessionList()
175
+
176
+ expect(sessionList.access).toBeDefined()
177
+ expect(sessionList.access?.operation).toBeDefined()
178
+ })
179
+
180
+ it('should deny querying sessions without session', () => {
181
+ const sessionList = createSessionList()
182
+ const queryAccess = sessionList.access?.operation?.query
183
+
184
+ expect(queryAccess?.({})).toBe(false)
185
+ expect(queryAccess?.({ session: null })).toBe(false)
186
+ })
187
+
188
+ it('should allow querying own sessions with filter', () => {
189
+ const sessionList = createSessionList()
190
+ const queryAccess = sessionList.access?.operation?.query
191
+
192
+ const result = queryAccess?.({ session: { userId: 'user-1' } })
193
+ expect(result).toEqual({
194
+ user: { id: { equals: 'user-1' } },
195
+ })
196
+ })
197
+
198
+ it('should allow creating sessions', () => {
199
+ const sessionList = createSessionList()
200
+ const createAccess = sessionList.access?.operation?.create
201
+
202
+ expect(createAccess?.({})).toBe(true)
203
+ })
204
+
205
+ it('should deny manual session updates', () => {
206
+ const sessionList = createSessionList()
207
+ const updateAccess = sessionList.access?.operation?.update
208
+
209
+ expect(updateAccess?.({})).toBe(false)
210
+ })
211
+ })
212
+
213
+ describe('createAccountList', () => {
214
+ it('should create Account list with required fields', () => {
215
+ const accountList = createAccountList()
216
+
217
+ expect(accountList.fields).toHaveProperty('accountId')
218
+ expect(accountList.fields).toHaveProperty('providerId')
219
+ expect(accountList.fields).toHaveProperty('user')
220
+ expect(accountList.fields).toHaveProperty('accessToken')
221
+ expect(accountList.fields).toHaveProperty('refreshToken')
222
+ expect(accountList.fields).toHaveProperty('password')
223
+ })
224
+
225
+ it('should have accountId field marked as required', () => {
226
+ const accountList = createAccountList()
227
+
228
+ expect(accountList.fields.accountId.validation?.isRequired).toBe(true)
229
+ })
230
+
231
+ it('should have providerId field marked as required', () => {
232
+ const accountList = createAccountList()
233
+
234
+ expect(accountList.fields.providerId.validation?.isRequired).toBe(true)
235
+ })
236
+
237
+ it('should have user relationship', () => {
238
+ const accountList = createAccountList()
239
+
240
+ expect(accountList.fields.user.type).toBe('relationship')
241
+ expect(accountList.fields.user.ref).toBe('User.accounts')
242
+ })
243
+
244
+ it('should deny querying accounts without session', () => {
245
+ const accountList = createAccountList()
246
+ const queryAccess = accountList.access?.operation?.query
247
+
248
+ expect(queryAccess?.({})).toBe(false)
249
+ expect(queryAccess?.({ session: null })).toBe(false)
250
+ })
251
+
252
+ it('should allow querying own accounts with filter', () => {
253
+ const accountList = createAccountList()
254
+ const queryAccess = accountList.access?.operation?.query
255
+
256
+ const result = queryAccess?.({ session: { userId: 'user-1' } })
257
+ expect(result).toEqual({
258
+ user: { id: { equals: 'user-1' } },
259
+ })
260
+ })
261
+
262
+ it('should allow users to update their own accounts', () => {
263
+ const accountList = createAccountList()
264
+ const updateAccess = accountList.access?.operation?.update
265
+
266
+ expect(
267
+ updateAccess?.({
268
+ session: { userId: 'user-1' },
269
+ item: { user: { id: 'user-1' } },
270
+ }),
271
+ ).toBe(true)
272
+ })
273
+
274
+ it('should deny users from updating other accounts', () => {
275
+ const accountList = createAccountList()
276
+ const updateAccess = accountList.access?.operation?.update
277
+
278
+ expect(
279
+ updateAccess?.({
280
+ session: { userId: 'user-1' },
281
+ item: { user: { id: 'user-2' } },
282
+ }),
283
+ ).toBe(false)
284
+ })
285
+ })
286
+
287
+ describe('createVerificationList', () => {
288
+ it('should create Verification list with required fields', () => {
289
+ const verificationList = createVerificationList()
290
+
291
+ expect(verificationList.fields).toHaveProperty('identifier')
292
+ expect(verificationList.fields).toHaveProperty('value')
293
+ expect(verificationList.fields).toHaveProperty('expiresAt')
294
+ })
295
+
296
+ it('should have identifier field marked as required', () => {
297
+ const verificationList = createVerificationList()
298
+
299
+ expect(verificationList.fields.identifier.validation?.isRequired).toBe(true)
300
+ })
301
+
302
+ it('should have value field marked as required', () => {
303
+ const verificationList = createVerificationList()
304
+
305
+ expect(verificationList.fields.value.validation?.isRequired).toBe(true)
306
+ })
307
+
308
+ it('should deny querying verification tokens', () => {
309
+ const verificationList = createVerificationList()
310
+ const queryAccess = verificationList.access?.operation?.query
311
+
312
+ expect(queryAccess?.({})).toBe(false)
313
+ })
314
+
315
+ it('should allow creating verification tokens', () => {
316
+ const verificationList = createVerificationList()
317
+ const createAccess = verificationList.access?.operation?.create
318
+
319
+ expect(createAccess?.({})).toBe(true)
320
+ })
321
+
322
+ it('should deny updates to verification tokens', () => {
323
+ const verificationList = createVerificationList()
324
+ const updateAccess = verificationList.access?.operation?.update
325
+
326
+ expect(updateAccess?.({})).toBe(false)
327
+ })
328
+
329
+ it('should allow deleting verification tokens', () => {
330
+ const verificationList = createVerificationList()
331
+ const deleteAccess = verificationList.access?.operation?.delete
332
+
333
+ expect(deleteAccess?.({})).toBe(true)
334
+ })
335
+ })
336
+
337
+ describe('getAuthLists', () => {
338
+ it('should return all auth lists', () => {
339
+ const lists = getAuthLists()
340
+
341
+ expect(lists).toHaveProperty('User')
342
+ expect(lists).toHaveProperty('Session')
343
+ expect(lists).toHaveProperty('Account')
344
+ expect(lists).toHaveProperty('Verification')
345
+ })
346
+
347
+ it('should pass user config to createUserList', () => {
348
+ const lists = getAuthLists({
349
+ fields: {
350
+ role: text(),
351
+ },
352
+ })
353
+
354
+ expect(lists.User.fields).toHaveProperty('role')
355
+ })
356
+ })
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { convertTableToList, convertBetterAuthSchema } from '../src/server/schema-converter.js'
3
+
4
+ describe('convertTableToList', () => {
5
+ it('should convert string fields', () => {
6
+ const tableSchema = {
7
+ modelName: 'TestTable',
8
+ fields: {
9
+ name: { type: 'string', required: true },
10
+ bio: { type: 'string' },
11
+ },
12
+ }
13
+
14
+ const listConfig = convertTableToList('test_table', tableSchema)
15
+
16
+ expect(listConfig.fields).toHaveProperty('name')
17
+ expect(listConfig.fields).toHaveProperty('bio')
18
+ expect(listConfig.fields.name.type).toBe('text')
19
+ expect(listConfig.fields.name.validation?.isRequired).toBe(true)
20
+ expect(listConfig.fields.bio.validation?.isRequired).toBeUndefined()
21
+ })
22
+
23
+ it('should convert number fields', () => {
24
+ const tableSchema = {
25
+ modelName: 'TestTable',
26
+ fields: {
27
+ age: { type: 'number', required: true },
28
+ score: { type: 'number', defaultValue: 0 },
29
+ },
30
+ }
31
+
32
+ const listConfig = convertTableToList('test_table', tableSchema)
33
+
34
+ expect(listConfig.fields.age.type).toBe('integer')
35
+ expect(listConfig.fields.age.validation?.isRequired).toBe(true)
36
+ expect(listConfig.fields.score.defaultValue).toBe(0)
37
+ })
38
+
39
+ it('should convert boolean fields', () => {
40
+ const tableSchema = {
41
+ modelName: 'TestTable',
42
+ fields: {
43
+ active: { type: 'boolean', defaultValue: true },
44
+ verified: { type: 'boolean' },
45
+ },
46
+ }
47
+
48
+ const listConfig = convertTableToList('test_table', tableSchema)
49
+
50
+ expect(listConfig.fields.active.type).toBe('checkbox')
51
+ expect(listConfig.fields.active.defaultValue).toBe(true)
52
+ expect(listConfig.fields.verified.type).toBe('checkbox')
53
+ })
54
+
55
+ it('should convert date fields', () => {
56
+ const tableSchema = {
57
+ modelName: 'TestTable',
58
+ fields: {
59
+ expiresAt: { type: 'date' },
60
+ createdOn: { type: 'date', defaultValue: 'now' },
61
+ },
62
+ }
63
+
64
+ const listConfig = convertTableToList('test_table', tableSchema)
65
+
66
+ expect(listConfig.fields.expiresAt.type).toBe('timestamp')
67
+ expect(listConfig.fields.createdOn.type).toBe('timestamp')
68
+ expect(listConfig.fields.createdOn.defaultValue).toEqual({ kind: 'now' })
69
+ })
70
+
71
+ it('should handle unique fields', () => {
72
+ const tableSchema = {
73
+ modelName: 'TestTable',
74
+ fields: {
75
+ email: { type: 'string', unique: true },
76
+ },
77
+ }
78
+
79
+ const listConfig = convertTableToList('test_table', tableSchema)
80
+
81
+ expect(listConfig.fields.email.isIndexed).toBe('unique')
82
+ })
83
+
84
+ it('should skip system fields', () => {
85
+ const tableSchema = {
86
+ modelName: 'TestTable',
87
+ fields: {
88
+ id: { type: 'string', required: true },
89
+ name: { type: 'string' },
90
+ createdAt: { type: 'date' },
91
+ updatedAt: { type: 'date' },
92
+ },
93
+ }
94
+
95
+ const listConfig = convertTableToList('test_table', tableSchema)
96
+
97
+ // System fields should be skipped
98
+ expect(listConfig.fields).not.toHaveProperty('id')
99
+ expect(listConfig.fields).not.toHaveProperty('createdAt')
100
+ expect(listConfig.fields).not.toHaveProperty('updatedAt')
101
+ // Regular fields should be included
102
+ expect(listConfig.fields).toHaveProperty('name')
103
+ })
104
+
105
+ it('should handle reference fields as text', () => {
106
+ const tableSchema = {
107
+ modelName: 'TestTable',
108
+ fields: {
109
+ userId: {
110
+ type: 'string',
111
+ required: true,
112
+ references: {
113
+ model: 'User',
114
+ field: 'id',
115
+ },
116
+ },
117
+ },
118
+ }
119
+
120
+ const listConfig = convertTableToList('test_table', tableSchema)
121
+
122
+ expect(listConfig.fields).toHaveProperty('userId')
123
+ expect(listConfig.fields.userId.type).toBe('text')
124
+ expect(listConfig.fields.userId.validation?.isRequired).toBe(true)
125
+ })
126
+
127
+ it('should handle unknown field types', () => {
128
+ const tableSchema = {
129
+ modelName: 'TestTable',
130
+ fields: {
131
+ customField: { type: 'unknown-type' },
132
+ },
133
+ }
134
+
135
+ const listConfig = convertTableToList('test_table', tableSchema)
136
+
137
+ // Should default to text
138
+ expect(listConfig.fields.customField.type).toBe('text')
139
+ })
140
+
141
+ it('should apply default access control for User table', () => {
142
+ const tableSchema = {
143
+ modelName: 'User',
144
+ fields: {
145
+ name: { type: 'string' },
146
+ },
147
+ }
148
+
149
+ const listConfig = convertTableToList('user', tableSchema)
150
+
151
+ expect(listConfig.access).toBeDefined()
152
+ expect(listConfig.access?.operation?.query?.({})).toBe(true)
153
+ expect(listConfig.access?.operation?.create?.({})).toBe(true)
154
+ })
155
+
156
+ it('should apply default access control for Session table', () => {
157
+ const tableSchema = {
158
+ modelName: 'Session',
159
+ fields: {
160
+ token: { type: 'string' },
161
+ },
162
+ }
163
+
164
+ const listConfig = convertTableToList('session', tableSchema)
165
+
166
+ expect(listConfig.access?.operation?.query?.({ session: null })).toBe(false)
167
+ expect(listConfig.access?.operation?.create?.({})).toBe(true)
168
+ expect(listConfig.access?.operation?.update?.({})).toBe(false)
169
+ })
170
+
171
+ it('should apply default access control for Verification table', () => {
172
+ const tableSchema = {
173
+ modelName: 'Verification',
174
+ fields: {
175
+ value: { type: 'string' },
176
+ },
177
+ }
178
+
179
+ const listConfig = convertTableToList('verification', tableSchema)
180
+
181
+ expect(listConfig.access?.operation?.query?.({})).toBe(false)
182
+ expect(listConfig.access?.operation?.create?.({})).toBe(true)
183
+ expect(listConfig.access?.operation?.update?.({})).toBe(false)
184
+ expect(listConfig.access?.operation?.delete?.({})).toBe(true)
185
+ })
186
+
187
+ it('should apply restrictive default access for unknown tables', () => {
188
+ const tableSchema = {
189
+ modelName: 'UnknownTable',
190
+ fields: {
191
+ data: { type: 'string' },
192
+ },
193
+ }
194
+
195
+ const listConfig = convertTableToList('unknown_table', tableSchema)
196
+
197
+ expect(listConfig.access?.operation?.query?.({})).toBe(false)
198
+ expect(listConfig.access?.operation?.create?.({})).toBe(false)
199
+ expect(listConfig.access?.operation?.update?.({})).toBe(false)
200
+ expect(listConfig.access?.operation?.delete?.({})).toBe(false)
201
+ })
202
+
203
+ it('should handle OAuth application table', () => {
204
+ const tableSchema = {
205
+ modelName: 'OAuthApplication',
206
+ fields: {
207
+ clientId: { type: 'string', required: true },
208
+ userId: { type: 'string', required: true },
209
+ },
210
+ }
211
+
212
+ const listConfig = convertTableToList('oauthapplication', tableSchema)
213
+
214
+ expect(listConfig.access?.operation?.query?.({ session: null })).toBe(false)
215
+ expect(listConfig.access?.operation?.create?.({})).toBe(true)
216
+ })
217
+ })
218
+
219
+ describe('convertBetterAuthSchema', () => {
220
+ it('should convert multiple tables', () => {
221
+ const schema = {
222
+ user: {
223
+ modelName: 'User',
224
+ fields: {
225
+ name: { type: 'string' },
226
+ },
227
+ },
228
+ session: {
229
+ modelName: 'Session',
230
+ fields: {
231
+ token: { type: 'string' },
232
+ },
233
+ },
234
+ }
235
+
236
+ const lists = convertBetterAuthSchema(schema)
237
+
238
+ expect(lists).toHaveProperty('User')
239
+ expect(lists).toHaveProperty('Session')
240
+ })
241
+
242
+ it('should use modelName for list key', () => {
243
+ const schema = {
244
+ custom_table: {
245
+ modelName: 'CustomTable',
246
+ fields: {
247
+ data: { type: 'string' },
248
+ },
249
+ },
250
+ }
251
+
252
+ const lists = convertBetterAuthSchema(schema)
253
+
254
+ expect(lists).toHaveProperty('CustomTable')
255
+ expect(lists).not.toHaveProperty('custom_table')
256
+ })
257
+
258
+ it('should convert snake_case to PascalCase if no modelName', () => {
259
+ const schema = {
260
+ oauth_application: {
261
+ modelName: '',
262
+ fields: {
263
+ data: { type: 'string' },
264
+ },
265
+ },
266
+ }
267
+
268
+ const lists = convertBetterAuthSchema(schema)
269
+
270
+ expect(lists).toHaveProperty('OauthApplication')
271
+ })
272
+
273
+ it('should handle empty schema', () => {
274
+ const lists = convertBetterAuthSchema({})
275
+
276
+ expect(lists).toEqual({})
277
+ })
278
+
279
+ it('should convert complex schema with multiple field types', () => {
280
+ const schema = {
281
+ test_table: {
282
+ modelName: 'TestTable',
283
+ fields: {
284
+ name: { type: 'string', required: true },
285
+ age: { type: 'number', defaultValue: 0 },
286
+ active: { type: 'boolean', defaultValue: true },
287
+ expiresAt: { type: 'date', defaultValue: 'now' },
288
+ userId: {
289
+ type: 'string',
290
+ references: { model: 'User', field: 'id' },
291
+ },
292
+ },
293
+ },
294
+ }
295
+
296
+ const lists = convertBetterAuthSchema(schema)
297
+
298
+ expect(lists.TestTable.fields).toHaveProperty('name')
299
+ expect(lists.TestTable.fields).toHaveProperty('age')
300
+ expect(lists.TestTable.fields).toHaveProperty('active')
301
+ expect(lists.TestTable.fields).toHaveProperty('expiresAt')
302
+ expect(lists.TestTable.fields).toHaveProperty('userId')
303
+ })
304
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["vitest/globals", "node"]
5
+ },
6
+ "include": ["src/**/*", "tests/**/*"]
7
+ }