@opensaas/stack-core 0.1.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/.turbo/turbo-build.log +4 -0
- package/README.md +447 -0
- package/dist/access/engine.d.ts +73 -0
- package/dist/access/engine.d.ts.map +1 -0
- package/dist/access/engine.js +244 -0
- package/dist/access/engine.js.map +1 -0
- package/dist/access/field-transforms.d.ts +47 -0
- package/dist/access/field-transforms.d.ts.map +1 -0
- package/dist/access/field-transforms.js +2 -0
- package/dist/access/field-transforms.js.map +1 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +2 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/types.d.ts +83 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/types.js +2 -0
- package/dist/access/types.js.map +1 -0
- package/dist/config/index.d.ts +39 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +413 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/context/index.d.ts +31 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +524 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/nested-operations.d.ts +10 -0
- package/dist/context/nested-operations.d.ts.map +1 -0
- package/dist/context/nested-operations.js +261 -0
- package/dist/context/nested-operations.js.map +1 -0
- package/dist/fields/index.d.ts +78 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/index.js +381 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/hooks/index.d.ts +58 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +79 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/case-utils.d.ts +49 -0
- package/dist/lib/case-utils.d.ts.map +1 -0
- package/dist/lib/case-utils.js +68 -0
- package/dist/lib/case-utils.js.map +1 -0
- package/dist/lib/case-utils.test.d.ts +2 -0
- package/dist/lib/case-utils.test.d.ts.map +1 -0
- package/dist/lib/case-utils.test.js +101 -0
- package/dist/lib/case-utils.test.js.map +1 -0
- package/dist/utils/password.d.ts +81 -0
- package/dist/utils/password.d.ts.map +1 -0
- package/dist/utils/password.js +132 -0
- package/dist/utils/password.js.map +1 -0
- package/dist/validation/schema.d.ts +17 -0
- package/dist/validation/schema.d.ts.map +1 -0
- package/dist/validation/schema.js +42 -0
- package/dist/validation/schema.js.map +1 -0
- package/dist/validation/schema.test.d.ts +2 -0
- package/dist/validation/schema.test.d.ts.map +1 -0
- package/dist/validation/schema.test.js +143 -0
- package/dist/validation/schema.test.js.map +1 -0
- package/docs/type-distribution-fix.md +136 -0
- package/package.json +48 -0
- package/src/access/engine.ts +360 -0
- package/src/access/field-transforms.ts +99 -0
- package/src/access/index.ts +20 -0
- package/src/access/types.ts +103 -0
- package/src/config/index.ts +71 -0
- package/src/config/types.ts +478 -0
- package/src/context/index.ts +814 -0
- package/src/context/nested-operations.ts +412 -0
- package/src/fields/index.ts +438 -0
- package/src/hooks/index.ts +132 -0
- package/src/index.ts +62 -0
- package/src/lib/case-utils.test.ts +127 -0
- package/src/lib/case-utils.ts +74 -0
- package/src/utils/password.ts +147 -0
- package/src/validation/schema.test.ts +171 -0
- package/src/validation/schema.ts +59 -0
- package/tests/access-relationships.test.ts +613 -0
- package/tests/access.test.ts +499 -0
- package/tests/config.test.ts +195 -0
- package/tests/context.test.ts +248 -0
- package/tests/hooks.test.ts +417 -0
- package/tests/password-type-distribution.test.ts +155 -0
- package/tests/password-types.test.ts +147 -0
- package/tests/password.test.ts +249 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# @opensaas/stack-core
|
|
2
|
+
|
|
3
|
+
Core OpenSaas Stack - config system, field types, access control, and code generation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @opensaas/stack-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 📝 **Schema Definition** - Config-first approach to defining your data model
|
|
14
|
+
- 🔒 **Access Control** - Automatic enforcement at database layer
|
|
15
|
+
- 🎯 **Type Generation** - Generate TypeScript types and Prisma schema
|
|
16
|
+
- 🔄 **Field Types** - Extensible field type system
|
|
17
|
+
- 🪝 **Hooks** - Data transformation and validation lifecycle
|
|
18
|
+
- 🛡️ **AI-Safe** - Silent failures prevent information leakage
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Define Your Schema
|
|
23
|
+
|
|
24
|
+
Create `opensaas.config.ts`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { config, list } from '@opensaas/stack-core'
|
|
28
|
+
import { text, integer, select, relationship } from '@opensaas/stack-core/fields'
|
|
29
|
+
import type { AccessControl } from '@opensaas/stack-core'
|
|
30
|
+
|
|
31
|
+
const isSignedIn: AccessControl = ({ session }) => !!session
|
|
32
|
+
|
|
33
|
+
const isAuthor: AccessControl = ({ session }) => {
|
|
34
|
+
if (!session) return false
|
|
35
|
+
return { authorId: { equals: session.userId } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default config({
|
|
39
|
+
db: {
|
|
40
|
+
provider: 'postgresql',
|
|
41
|
+
url: process.env.DATABASE_URL,
|
|
42
|
+
},
|
|
43
|
+
lists: {
|
|
44
|
+
User: list({
|
|
45
|
+
fields: {
|
|
46
|
+
name: text({ validation: { isRequired: true } }),
|
|
47
|
+
email: text({ isIndexed: 'unique' }),
|
|
48
|
+
posts: relationship({ ref: 'Post.author', many: true }),
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
Post: list({
|
|
52
|
+
fields: {
|
|
53
|
+
title: text({ validation: { isRequired: true } }),
|
|
54
|
+
slug: text({ isIndexed: 'unique' }),
|
|
55
|
+
content: text(),
|
|
56
|
+
status: select({
|
|
57
|
+
options: [
|
|
58
|
+
{ label: 'Draft', value: 'draft' },
|
|
59
|
+
{ label: 'Published', value: 'published' },
|
|
60
|
+
],
|
|
61
|
+
defaultValue: 'draft',
|
|
62
|
+
}),
|
|
63
|
+
author: relationship({ ref: 'User.posts' }),
|
|
64
|
+
internalNotes: text({
|
|
65
|
+
access: {
|
|
66
|
+
read: isAuthor,
|
|
67
|
+
create: isAuthor,
|
|
68
|
+
update: isAuthor,
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
access: {
|
|
73
|
+
operation: {
|
|
74
|
+
query: ({ session }) => {
|
|
75
|
+
if (!session) return { status: { equals: 'published' } }
|
|
76
|
+
return true
|
|
77
|
+
},
|
|
78
|
+
create: isSignedIn,
|
|
79
|
+
update: isAuthor,
|
|
80
|
+
delete: isAuthor,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Generate Schema and Types
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
opensaas generate
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This creates:
|
|
95
|
+
|
|
96
|
+
- `prisma/schema.prisma` - Prisma schema
|
|
97
|
+
- `.opensaas/types.ts` - TypeScript types
|
|
98
|
+
|
|
99
|
+
### 3. Create Context
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// lib/context.ts
|
|
103
|
+
import { getContext } from '@opensaas/stack-core'
|
|
104
|
+
import { PrismaClient } from '@prisma/client'
|
|
105
|
+
import config from '../opensaas.config'
|
|
106
|
+
|
|
107
|
+
export const prisma = new PrismaClient()
|
|
108
|
+
|
|
109
|
+
export async function getContextWithUser(userId: string) {
|
|
110
|
+
return getContext(config, prisma, { userId })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getContext() {
|
|
114
|
+
return getContext(config, prisma, null)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 4. Use in Your App
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { getContextWithUser } from './lib/context'
|
|
122
|
+
|
|
123
|
+
export async function createPost(userId: string, data: any) {
|
|
124
|
+
const context = getContextWithUser(userId)
|
|
125
|
+
|
|
126
|
+
// Access control automatically enforced
|
|
127
|
+
const post = await context.db.post.create({ data })
|
|
128
|
+
|
|
129
|
+
if (!post) {
|
|
130
|
+
return { error: 'Access denied' }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { post }
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Field Types
|
|
138
|
+
|
|
139
|
+
### Available Fields
|
|
140
|
+
|
|
141
|
+
- **text()** - String field
|
|
142
|
+
- **integer()** - Number field
|
|
143
|
+
- **checkbox()** - Boolean field
|
|
144
|
+
- **timestamp()** - Date/time field
|
|
145
|
+
- **password()** - Password field (excluded from reads)
|
|
146
|
+
- **select()** - Enum field with options
|
|
147
|
+
- **relationship()** - Foreign key relationship
|
|
148
|
+
|
|
149
|
+
### Field Options
|
|
150
|
+
|
|
151
|
+
All fields support:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
text({
|
|
155
|
+
validation: {
|
|
156
|
+
isRequired: true,
|
|
157
|
+
length: { min: 3, max: 100 },
|
|
158
|
+
},
|
|
159
|
+
isIndexed: 'unique', // or true for non-unique index
|
|
160
|
+
defaultValue: 'Hello',
|
|
161
|
+
access: {
|
|
162
|
+
read: ({ session }) => !!session,
|
|
163
|
+
create: ({ session }) => !!session,
|
|
164
|
+
update: ({ session }) => !!session,
|
|
165
|
+
},
|
|
166
|
+
hooks: {
|
|
167
|
+
resolveInput: async ({ resolvedData }) => resolvedData,
|
|
168
|
+
validateInput: async ({ resolvedData }) => {
|
|
169
|
+
/* validate */
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
ui: {
|
|
173
|
+
fieldType: 'custom', // Reference global component
|
|
174
|
+
component: CustomComponent, // Or provide directly
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Creating Custom Field Types
|
|
180
|
+
|
|
181
|
+
Field types are fully self-contained:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import type { BaseFieldConfig } from '@opensaas/stack-core'
|
|
185
|
+
import { z } from 'zod'
|
|
186
|
+
|
|
187
|
+
export type MyCustomField = BaseFieldConfig & {
|
|
188
|
+
type: 'myCustom'
|
|
189
|
+
customOption?: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function myCustom(options?: Omit<MyCustomField, 'type'>): MyCustomField {
|
|
193
|
+
return {
|
|
194
|
+
type: 'myCustom',
|
|
195
|
+
...options,
|
|
196
|
+
getZodSchema: (fieldName, operation) => {
|
|
197
|
+
return z.string().optional()
|
|
198
|
+
},
|
|
199
|
+
getPrismaType: (fieldName) => {
|
|
200
|
+
return { type: 'String', modifiers: '?' }
|
|
201
|
+
},
|
|
202
|
+
getTypeScriptType: () => {
|
|
203
|
+
return { type: 'string', optional: true }
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Access Control
|
|
210
|
+
|
|
211
|
+
### Operation-Level Access
|
|
212
|
+
|
|
213
|
+
Control who can query, create, update, or delete:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
access: {
|
|
217
|
+
operation: {
|
|
218
|
+
query: true, // Everyone can read
|
|
219
|
+
create: isSignedIn, // Must be signed in
|
|
220
|
+
update: isAuthor, // Only author
|
|
221
|
+
delete: isAuthor, // Only author
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Filter-Based Access
|
|
227
|
+
|
|
228
|
+
Return Prisma filters to scope access:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const isAuthor: AccessControl = ({ session }) => {
|
|
232
|
+
if (!session) return false
|
|
233
|
+
return { authorId: { equals: session.userId } }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Applied as: where: { AND: [userFilter, { authorId: { equals: userId } }] }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Field-Level Access
|
|
240
|
+
|
|
241
|
+
Control access to individual fields:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
internalNotes: text({
|
|
245
|
+
access: {
|
|
246
|
+
read: isAuthor, // Only author can see
|
|
247
|
+
create: isAuthor, // Only author can set on create
|
|
248
|
+
update: isAuthor, // Only author can modify
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Silent Failures
|
|
254
|
+
|
|
255
|
+
Access-denied operations return `null` or `[]` instead of throwing:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const post = await context.db.post.update({
|
|
259
|
+
where: { id: postId },
|
|
260
|
+
data: { title: 'New Title' },
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (!post) {
|
|
264
|
+
// Either doesn't exist OR user lacks access
|
|
265
|
+
// No information leaked about which
|
|
266
|
+
return { error: 'Not found' }
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Hooks
|
|
271
|
+
|
|
272
|
+
Transform and validate data during operations:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
hooks: {
|
|
276
|
+
// Transform input before validation
|
|
277
|
+
resolveInput: async ({ resolvedData, operation, session }) => {
|
|
278
|
+
if (operation === 'create') {
|
|
279
|
+
return { ...resolvedData, createdBy: session.userId }
|
|
280
|
+
}
|
|
281
|
+
return resolvedData
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Custom validation
|
|
285
|
+
validateInput: async ({ resolvedData, fieldPath }) => {
|
|
286
|
+
if (resolvedData.title?.includes('spam')) {
|
|
287
|
+
throw new Error('Title contains prohibited content')
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// Before database operation
|
|
292
|
+
beforeOperation: async ({ operation, resolvedData }) => {
|
|
293
|
+
console.log(`About to ${operation}`, resolvedData)
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// After database operation
|
|
297
|
+
afterOperation: async ({ operation, item }) => {
|
|
298
|
+
if (operation === 'create') {
|
|
299
|
+
await sendNotification(item)
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Hook Execution Order
|
|
306
|
+
|
|
307
|
+
1. `resolveInput` - Transform input
|
|
308
|
+
2. `validateInput` - Custom validation
|
|
309
|
+
3. Field validation - Built-in rules
|
|
310
|
+
4. Field-level access - Filter writable fields
|
|
311
|
+
5. `beforeOperation` - Pre-operation side effects
|
|
312
|
+
6. **Database operation**
|
|
313
|
+
7. `afterOperation` - Post-operation side effects
|
|
314
|
+
|
|
315
|
+
## Context API
|
|
316
|
+
|
|
317
|
+
### Creating Context
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { getContext } from '@opensaas/stack-core'
|
|
321
|
+
|
|
322
|
+
// With session
|
|
323
|
+
const context = getContext(config, prisma, { userId: '123' })
|
|
324
|
+
|
|
325
|
+
// Anonymous
|
|
326
|
+
const context = getContext(config, prisma, null)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Using Context
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// All Prisma operations supported
|
|
333
|
+
const post = await context.db.post.create({ data })
|
|
334
|
+
const posts = await context.db.post.findMany()
|
|
335
|
+
const post = await context.db.post.findUnique({ where: { id } })
|
|
336
|
+
const post = await context.db.post.update({ where: { id }, data })
|
|
337
|
+
const post = await context.db.post.delete({ where: { id } })
|
|
338
|
+
|
|
339
|
+
// Access control is automatic
|
|
340
|
+
// Returns null/[] if access denied
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Generators
|
|
344
|
+
|
|
345
|
+
### Prisma Schema
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { writePrismaSchema } from '@opensaas/stack-core'
|
|
349
|
+
|
|
350
|
+
writePrismaSchema(config, './prisma/schema.prisma')
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### TypeScript Types
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { writeTypes } from '@opensaas/stack-core'
|
|
357
|
+
|
|
358
|
+
writeTypes(config, './.opensaas/types.ts')
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Utility Functions
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { getDbKey, getUrlKey, getListKeyFromUrl } from '@opensaas/stack-core'
|
|
365
|
+
|
|
366
|
+
getDbKey('BlogPost') // 'blogPost' - for context.db access
|
|
367
|
+
getUrlKey('BlogPost') // 'blog-post' - for URLs
|
|
368
|
+
getListKeyFromUrl('blog-post') // 'BlogPost' - parse from URLs
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Validation
|
|
372
|
+
|
|
373
|
+
Built-in validation with Zod:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
text({
|
|
377
|
+
validation: {
|
|
378
|
+
isRequired: true,
|
|
379
|
+
length: { min: 3, max: 100 },
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
integer({
|
|
384
|
+
validation: {
|
|
385
|
+
isRequired: true,
|
|
386
|
+
min: 0,
|
|
387
|
+
max: 1000,
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Custom validation in hooks:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
hooks: {
|
|
396
|
+
validateInput: async ({ resolvedData }) => {
|
|
397
|
+
const { title } = resolvedData
|
|
398
|
+
if (title && !isValidSlug(slugify(title))) {
|
|
399
|
+
throw new ValidationError('Title contains invalid characters')
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Testing
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import { describe, it, expect } from 'vitest'
|
|
409
|
+
import { getContext } from '@opensaas/stack-core'
|
|
410
|
+
import config from './opensaas.config'
|
|
411
|
+
|
|
412
|
+
describe('Post access control', () => {
|
|
413
|
+
it('allows author to update their post', async () => {
|
|
414
|
+
const context = getContext(config, prisma, { userId: authorId })
|
|
415
|
+
const updated = await context.db.post.update({
|
|
416
|
+
where: { id: postId },
|
|
417
|
+
data: { title: 'New Title' },
|
|
418
|
+
})
|
|
419
|
+
expect(updated).toBeTruthy()
|
|
420
|
+
expect(updated?.title).toBe('New Title')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('denies non-author from updating post', async () => {
|
|
424
|
+
const context = getContext(config, prisma, { userId: otherUserId })
|
|
425
|
+
const updated = await context.db.post.update({
|
|
426
|
+
where: { id: postId },
|
|
427
|
+
data: { title: 'Hacked!' },
|
|
428
|
+
})
|
|
429
|
+
expect(updated).toBeNull() // Silent failure
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Examples
|
|
435
|
+
|
|
436
|
+
- [Blog Example](../../examples/blog) - Complete working example
|
|
437
|
+
- [Custom Field Example](../../examples/custom-field) - Extending field types
|
|
438
|
+
|
|
439
|
+
## Learn More
|
|
440
|
+
|
|
441
|
+
- [API Reference](../../docs/API.md) - Complete API documentation
|
|
442
|
+
- [OpenSaas Stack](../../README.md) - Stack overview
|
|
443
|
+
- [CLAUDE.md](../../CLAUDE.md) - Development guide
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AccessControl, Session, AccessContext, PrismaFilter } from './types.js';
|
|
2
|
+
import type { FieldAccess } from './types.js';
|
|
3
|
+
import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Check if access control result is a boolean
|
|
6
|
+
*/
|
|
7
|
+
export declare function isBoolean(value: unknown): value is boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Check if access control result is a Prisma filter
|
|
10
|
+
*/
|
|
11
|
+
export declare function isPrismaFilter(value: unknown): value is PrismaFilter;
|
|
12
|
+
/**
|
|
13
|
+
* Parse a relationship ref and get the related list configuration
|
|
14
|
+
* Relationship refs are in the format "ListName.fieldName"
|
|
15
|
+
*
|
|
16
|
+
* @param relationshipRef - The ref string (e.g., "Post.author")
|
|
17
|
+
* @param config - The OpenSaas configuration
|
|
18
|
+
* @returns The related list name and config, or null if not found
|
|
19
|
+
*/
|
|
20
|
+
export declare function getRelatedListConfig(relationshipRef: string, config: OpenSaasConfig): {
|
|
21
|
+
listName: string;
|
|
22
|
+
listConfig: ListConfig;
|
|
23
|
+
} | null;
|
|
24
|
+
/**
|
|
25
|
+
* Execute an access control function
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkAccess<T = Record<string, unknown>>(accessControl: AccessControl<T> | undefined, args: {
|
|
28
|
+
session: Session;
|
|
29
|
+
item?: T;
|
|
30
|
+
context: AccessContext;
|
|
31
|
+
}): Promise<boolean | PrismaFilter<T>>;
|
|
32
|
+
/**
|
|
33
|
+
* Merge user filter with access control filter
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergeFilters(userFilter: PrismaFilter | undefined, accessFilter: boolean | PrismaFilter): PrismaFilter | null;
|
|
36
|
+
/**
|
|
37
|
+
* Check field-level access for a specific operation
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, operation: 'read' | 'create' | 'update', args: {
|
|
40
|
+
session: Session;
|
|
41
|
+
item?: Record<string, unknown>;
|
|
42
|
+
context: AccessContext;
|
|
43
|
+
}): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Build Prisma include object with access control filters
|
|
46
|
+
* This allows us to filter relationships at the database level instead of in memory
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildIncludeWithAccessControl(fieldConfigs: Record<string, FieldConfig>, args: {
|
|
49
|
+
session: Session;
|
|
50
|
+
context: AccessContext;
|
|
51
|
+
}, config: OpenSaasConfig, depth?: number): Promise<Record<string, boolean | {
|
|
52
|
+
where?: PrismaFilter;
|
|
53
|
+
include?: Record<string, boolean | /*elided*/ any>;
|
|
54
|
+
}> | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* Filter fields from an object based on read access
|
|
57
|
+
* Recursively applies access control to nested relationships
|
|
58
|
+
*/
|
|
59
|
+
export declare function filterReadableFields<T extends Record<string, unknown>>(item: T, fieldConfigs: Record<string, FieldConfig>, args: {
|
|
60
|
+
session: Session;
|
|
61
|
+
context: AccessContext;
|
|
62
|
+
}, config?: OpenSaasConfig, depth?: number): Promise<Partial<T>>;
|
|
63
|
+
/**
|
|
64
|
+
* Filter fields from input data based on write access (create/update)
|
|
65
|
+
*/
|
|
66
|
+
export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
|
|
67
|
+
access?: FieldAccess;
|
|
68
|
+
}>, operation: 'create' | 'update', args: {
|
|
69
|
+
session: Session;
|
|
70
|
+
item?: Record<string, unknown>;
|
|
71
|
+
context: AccessContext;
|
|
72
|
+
}): Promise<Partial<T>>;
|
|
73
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"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"}
|