v / examples / orm / orm_scope_middleware.v
269 lines · 229 sloc · 6.29 KB · 7b724e98ac187e233bd03716901c29f5e00f815f
Raw
1// orm_scope_middleware.v - ORM middleware pattern demo
2//
3// Shows how middleware manages DataScope (multi-tenant) transparently.
4// Business code only changes acquire() -> pool_acquire_scoped().
5//
6// Run: v run orm_scope_middleware.v
7
8module main
9
10import db.sqlite
11import orm
12
13// =============================================================================
14// Model
15// =============================================================================
16
17@[table: 'sys_users']
18struct SysUser {
19 id int @[primary; sql: serial]
20 name string
21 tenant_id int
22 org_id int
23}
24
25// =============================================================================
26// Request context
27// =============================================================================
28
29struct UserInfo {
30mut:
31 role string
32 tenant_id int
33}
34
35pub struct Context {
36pub mut:
37 user UserInfo
38 dbpool &DatabasePool
39}
40
41// =============================================================================
42// Connection pool (SQLite version)
43// In real projects, use adapter.dbpool.DatabasePoolable
44// =============================================================================
45
46pub struct DatabasePool {
47 conn &sqlite.DB
48}
49
50pub fn new_pool() &DatabasePool {
51 mut db := sqlite.connect(':memory:') or { panic(err) }
52 return &DatabasePool{
53 conn: &db
54 }
55}
56
57// acquire gets a connection from pool
58pub fn (mut p DatabasePool) acquire() !&sqlite.DB {
59 return p.conn
60}
61
62// release returns a connection to pool
63pub fn (mut p DatabasePool) release(conn &sqlite.DB) ! {
64 // no-op for single-connection SQLite
65 // real pool: p.inner.put(conn)!
66}
67
68// =============================================================================
69// Middleware layer -- pool_acquire_scoped
70//
71// Responsibilities:
72// 1. Acquire connection from pool
73// 2. Create orm.DB with DataScope injected
74// 3. Skip scope filters based on user role
75// 4. Return configured orm.DB
76//
77// Business code just replaces acquire() -> pool_acquire_scoped()
78// =============================================================================
79
80pub fn pool_acquire_scoped(mut ctx Context) !(orm.DB, &sqlite.DB) {
81 // 1. acquire raw connection
82 raw_conn := ctx.dbpool.acquire()!
83
84 // 2. create base orm.DB with tenant scope
85 base_db := orm.new_db(raw_conn, orm.DataScope{
86 filters: [
87 orm.QueryFilter{
88 field: 'tenant_id'
89 value: orm.Primitive(ctx.user.tenant_id)
90 mode: .dynamic
91 },
92 ]
93 })
94
95 // 3. skip scope filters by role
96 mut scoped_db := base_db
97 match ctx.user.role {
98 'admin' {
99 scoped_db = scoped_db.unscoped()
100 }
101 'manager' {
102 scoped_db = scoped_db.unscoped('org_id')
103 }
104 'normal' {}
105 else {}
106 }
107
108 // 4. return scoped DB
109 return scoped_db, raw_conn
110}
111
112// =============================================================================
113// Business layer -- Repository
114//
115// No awareness of DataScope at all.
116// Only change: acquire() -> pool_acquire_scoped()
117// =============================================================================
118
119fn get_users(mut ctx Context) ![]SysUser {
120 db, conn := pool_acquire_scoped(mut ctx)!
121 defer {
122 ctx.dbpool.release(conn) or {}
123 }
124
125 return sql db {
126 select from SysUser
127 }!
128}
129
130fn get_user_by_name(mut ctx Context, name string) ![]SysUser {
131 db, conn := pool_acquire_scoped(mut ctx)!
132 defer {
133 ctx.dbpool.release(conn) or {}
134 }
135
136 return sql db {
137 select from SysUser where name == name
138 }!
139}
140
141fn count_users(mut ctx Context) !int {
142 db, conn := pool_acquire_scoped(mut ctx)!
143 defer {
144 ctx.dbpool.release(conn) or {}
145 }
146
147 return sql db {
148 select count from SysUser
149 }!
150}
151
152// =============================================================================
153// Demo
154// =============================================================================
155
156fn main() {
157 // setup
158 mut pool := new_pool()
159 db := pool.conn
160
161 sql db {
162 create table SysUser
163 }!
164
165 users := [
166 SysUser{
167 name: 'Alice'
168 tenant_id: 1
169 org_id: 10
170 },
171 SysUser{
172 name: 'Bob'
173 tenant_id: 1
174 org_id: 20
175 },
176 SysUser{
177 name: 'Charlie'
178 tenant_id: 2
179 org_id: 10
180 },
181 SysUser{
182 name: 'Diana'
183 tenant_id: 2
184 org_id: 20
185 },
186 ]
187 for u in users {
188 sql db {
189 insert u into SysUser
190 }!
191 }
192
193 println('=== test data ===')
194 println('Alice : tenant=1, org=10')
195 println('Bob : tenant=1, org=20')
196 println('Charlie: tenant=2, org=10')
197 println('Diana : tenant=2, org=20')
198
199 mut ctx := Context{
200 user: UserInfo{
201 role: 'normal'
202 tenant_id: 1
203 }
204 dbpool: pool
205 }
206
207 // scenario 1: normal user, tenant=1
208 println('\n--- normal user (tenant=1) ---')
209 users1 := get_users(mut ctx) or { panic(err) }
210 println('got ${users1.len} rows:')
211 for u in users1 {
212 println(' - ${u.name} (tenant=${u.tenant_id})')
213 }
214
215 // scenario 2: manager, tenant=1 (skip org_id, no effect since only tenant_id scope)
216 println('\n--- manager user (tenant=1) ---')
217 ctx.user.role = 'manager'
218
219 users2 := get_users(mut ctx) or { panic(err) }
220 println('got ${users2.len} rows:')
221 for u in users2 {
222 println(' - ${u.name} (tenant=${u.tenant_id})')
223 }
224
225 // scenario 3: admin (skip all scopes)
226 println('\n--- admin user (skip all scopes) ---')
227 ctx.user.role = 'admin'
228
229 users3 := get_users(mut ctx) or { panic(err) }
230 println('got ${users3.len} rows:')
231 for u in users3 {
232 println(' - ${u.name} (tenant=${u.tenant_id})')
233 }
234
235 // scenario 4: normal user, tenant=2
236 println('\n--- normal user (tenant=2) ---')
237 ctx.user.role = 'normal'
238 ctx.user.tenant_id = 2
239
240 users4 := get_users(mut ctx) or { panic(err) }
241 println('got ${users4.len} rows:')
242 for u in users4 {
243 println(' - ${u.name} (tenant=${u.tenant_id})')
244 }
245
246 // scenario 5: get_user_by_name
247 println('\n--- get_user_by_name (admin, name=Alice) ---')
248 ctx.user.role = 'admin'
249 users_by_name := get_user_by_name(mut ctx, 'Alice') or { panic(err) }
250 println('got ${users_by_name.len} rows:')
251 for u in users_by_name {
252 println(' - ${u.name} (tenant=${u.tenant_id})')
253 }
254
255 // scenario 6: count
256 println('\n--- count (admin) ---')
257 total := count_users(mut ctx) or { panic(err) }
258 println('total: ${total}')
259
260 // assertions
261 println('\n=== assertions ===')
262 assert users1.len == 2 // normal/tenant=1: Alice + Bob
263 assert users2.len == 2 // manager/tenant=1: Alice + Bob
264 assert users3.len == 4 // admin: all 4
265 assert users4.len == 2 // normal/tenant=2: Charlie + Diana
266 assert users_by_name.len == 1 // admin: Alice
267 assert total == 4 // admin: all 4
268 println('all passed ✓')
269}
270