namespace-guard 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/README.md +502 -0
- package/dist/adapters/drizzle.d.mts +42 -0
- package/dist/adapters/drizzle.d.ts +42 -0
- package/dist/adapters/drizzle.js +55 -0
- package/dist/adapters/drizzle.mjs +30 -0
- package/dist/adapters/knex.d.mts +36 -0
- package/dist/adapters/knex.d.ts +36 -0
- package/dist/adapters/knex.js +39 -0
- package/dist/adapters/knex.mjs +14 -0
- package/dist/adapters/kysely.d.mts +37 -0
- package/dist/adapters/kysely.d.ts +37 -0
- package/dist/adapters/kysely.js +39 -0
- package/dist/adapters/kysely.mjs +14 -0
- package/dist/adapters/prisma.d.mts +36 -0
- package/dist/adapters/prisma.d.ts +36 -0
- package/dist/adapters/prisma.js +47 -0
- package/dist/adapters/prisma.mjs +22 -0
- package/dist/adapters/raw.d.mts +34 -0
- package/dist/adapters/raw.d.ts +34 -0
- package/dist/adapters/raw.js +40 -0
- package/dist/adapters/raw.mjs +15 -0
- package/dist/cli.d.mts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +297 -0
- package/dist/cli.mjs +273 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +166 -0
- package/dist/index.mjs +140 -0
- package/package.json +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# namespace-guard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/namespace-guard)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**[Live Demo](https://paultendo.github.io/namespace-guard/)** — try it in your browser
|
|
8
|
+
|
|
9
|
+
**Check slug/handle uniqueness across multiple database tables with reserved name protection.**
|
|
10
|
+
|
|
11
|
+
Perfect for multi-tenant apps where usernames, organization slugs, and reserved routes all share one URL namespace - like Twitter (`@username`), GitHub (`github.com/username`), or any SaaS with vanity URLs.
|
|
12
|
+
|
|
13
|
+
## The Problem
|
|
14
|
+
|
|
15
|
+
You have a URL structure like `yourapp.com/:slug` that could be:
|
|
16
|
+
- A user profile (`/sarah`)
|
|
17
|
+
- An organization (`/acme-corp`)
|
|
18
|
+
- A reserved route (`/settings`, `/admin`, `/api`)
|
|
19
|
+
|
|
20
|
+
When someone signs up or creates an org, you need to check that their chosen slug:
|
|
21
|
+
1. Isn't already taken by another user
|
|
22
|
+
2. Isn't already taken by an organization
|
|
23
|
+
3. Isn't a reserved system route
|
|
24
|
+
4. Follows your naming rules
|
|
25
|
+
|
|
26
|
+
This library handles all of that in one call.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install namespace-guard
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createNamespaceGuard } from "namespace-guard";
|
|
38
|
+
import { createPrismaAdapter } from "namespace-guard/adapters/prisma";
|
|
39
|
+
import { PrismaClient } from "@prisma/client";
|
|
40
|
+
|
|
41
|
+
const prisma = new PrismaClient();
|
|
42
|
+
|
|
43
|
+
// Define your namespace rules once
|
|
44
|
+
const guard = createNamespaceGuard(
|
|
45
|
+
{
|
|
46
|
+
reserved: ["admin", "api", "settings", "dashboard", "login", "signup"],
|
|
47
|
+
sources: [
|
|
48
|
+
{ name: "user", column: "handle", scopeKey: "id" },
|
|
49
|
+
{ name: "organization", column: "slug", scopeKey: "id" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
createPrismaAdapter(prisma)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Check if a slug is available
|
|
56
|
+
const result = await guard.check("acme-corp");
|
|
57
|
+
|
|
58
|
+
if (result.available) {
|
|
59
|
+
// Create the org
|
|
60
|
+
} else {
|
|
61
|
+
// Show error: result.message
|
|
62
|
+
// e.g., "That name is reserved." or "That name is already in use."
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Why namespace-guard?
|
|
67
|
+
|
|
68
|
+
| Feature | namespace-guard | DIY Solution |
|
|
69
|
+
|---------|-----------------|--------------|
|
|
70
|
+
| Multi-table uniqueness | One call | Multiple queries |
|
|
71
|
+
| Reserved name blocking | Built-in with categories | Manual list checking |
|
|
72
|
+
| Ownership scoping | No false positives on self-update | Easy to forget |
|
|
73
|
+
| Format validation | Configurable regex | Scattered validation |
|
|
74
|
+
| Conflict suggestions | Auto-suggest alternatives | Not built |
|
|
75
|
+
| Async validators | Custom hooks (profanity, etc.) | Manual wiring |
|
|
76
|
+
| Batch checking | `checkMany()` | Loop it yourself |
|
|
77
|
+
| ORM agnostic | Prisma, Drizzle, Kysely, Knex, raw SQL | Tied to your ORM |
|
|
78
|
+
| CLI | `npx namespace-guard check` | None |
|
|
79
|
+
|
|
80
|
+
## Adapters
|
|
81
|
+
|
|
82
|
+
### Prisma
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { PrismaClient } from "@prisma/client";
|
|
86
|
+
import { createPrismaAdapter } from "namespace-guard/adapters/prisma";
|
|
87
|
+
|
|
88
|
+
const prisma = new PrismaClient();
|
|
89
|
+
const adapter = createPrismaAdapter(prisma);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Drizzle
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { eq } from "drizzle-orm";
|
|
96
|
+
import { createDrizzleAdapter } from "namespace-guard/adapters/drizzle";
|
|
97
|
+
import { db } from "./db";
|
|
98
|
+
import { users, organizations } from "./schema";
|
|
99
|
+
|
|
100
|
+
const adapter = createDrizzleAdapter(db, { users, organizations }, eq);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Kysely
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { Kysely, PostgresDialect } from "kysely";
|
|
107
|
+
import { createKyselyAdapter } from "namespace-guard/adapters/kysely";
|
|
108
|
+
|
|
109
|
+
const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });
|
|
110
|
+
const adapter = createKyselyAdapter(db);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Knex
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import Knex from "knex";
|
|
117
|
+
import { createKnexAdapter } from "namespace-guard/adapters/knex";
|
|
118
|
+
|
|
119
|
+
const knex = Knex({ client: "pg", connection: process.env.DATABASE_URL });
|
|
120
|
+
const adapter = createKnexAdapter(knex);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Raw SQL (pg, mysql2, etc.)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { Pool } from "pg";
|
|
127
|
+
import { createRawAdapter } from "namespace-guard/adapters/raw";
|
|
128
|
+
|
|
129
|
+
const pool = new Pool();
|
|
130
|
+
const adapter = createRawAdapter((sql, params) => pool.query(sql, params));
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const guard = createNamespaceGuard({
|
|
137
|
+
// Reserved names - flat list, Set, or categorized
|
|
138
|
+
reserved: new Set([
|
|
139
|
+
"admin",
|
|
140
|
+
"api",
|
|
141
|
+
"settings",
|
|
142
|
+
"dashboard",
|
|
143
|
+
"login",
|
|
144
|
+
"signup",
|
|
145
|
+
"help",
|
|
146
|
+
"support",
|
|
147
|
+
"billing",
|
|
148
|
+
]),
|
|
149
|
+
|
|
150
|
+
// Data sources to check for collisions
|
|
151
|
+
// Queries run in parallel for speed
|
|
152
|
+
sources: [
|
|
153
|
+
{
|
|
154
|
+
name: "user", // Prisma model / Drizzle table / SQL table name
|
|
155
|
+
column: "handle", // Column containing the slug/handle
|
|
156
|
+
idColumn: "id", // Primary key column (default: "id")
|
|
157
|
+
scopeKey: "id", // Key for ownership checks (see below)
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "organization",
|
|
161
|
+
column: "slug",
|
|
162
|
+
scopeKey: "id",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "team",
|
|
166
|
+
column: "slug",
|
|
167
|
+
scopeKey: "id",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
|
|
171
|
+
// Validation pattern (default: /^[a-z0-9][a-z0-9-]{1,29}$/)
|
|
172
|
+
// This default requires: 2-30 chars, lowercase alphanumeric + hyphens, can't start with hyphen
|
|
173
|
+
pattern: /^[a-z0-9][a-z0-9-]{2,39}$/,
|
|
174
|
+
|
|
175
|
+
// Custom error messages
|
|
176
|
+
messages: {
|
|
177
|
+
invalid: "Use 3-40 lowercase letters, numbers, or hyphens.",
|
|
178
|
+
reserved: "That name is reserved. Please choose another.",
|
|
179
|
+
taken: (sourceName) => `That name is already taken.`,
|
|
180
|
+
},
|
|
181
|
+
}, adapter);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Reserved Name Categories
|
|
185
|
+
|
|
186
|
+
Group reserved names by category with different error messages:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const guard = createNamespaceGuard({
|
|
190
|
+
reserved: {
|
|
191
|
+
system: ["admin", "api", "settings", "dashboard"],
|
|
192
|
+
brand: ["oncor", "bandcamp"],
|
|
193
|
+
offensive: ["..."],
|
|
194
|
+
},
|
|
195
|
+
sources: [/* ... */],
|
|
196
|
+
messages: {
|
|
197
|
+
reserved: {
|
|
198
|
+
system: "That's a system route.",
|
|
199
|
+
brand: "That's a protected brand name.",
|
|
200
|
+
offensive: "That name is not allowed.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}, adapter);
|
|
204
|
+
|
|
205
|
+
const result = await guard.check("admin");
|
|
206
|
+
// { available: false, reason: "reserved", category: "system", message: "That's a system route." }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
You can also use a single string message for all categories, or mix — categories without a specific message fall back to the default.
|
|
210
|
+
|
|
211
|
+
## Async Validators
|
|
212
|
+
|
|
213
|
+
Add custom async checks that run after format/reserved validation but before database queries:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const guard = createNamespaceGuard({
|
|
217
|
+
sources: [/* ... */],
|
|
218
|
+
validators: [
|
|
219
|
+
async (identifier) => {
|
|
220
|
+
if (await isProfane(identifier)) {
|
|
221
|
+
return { available: false, message: "That name is not allowed." };
|
|
222
|
+
}
|
|
223
|
+
return null; // pass
|
|
224
|
+
},
|
|
225
|
+
async (identifier) => {
|
|
226
|
+
if (await isTrademarkViolation(identifier)) {
|
|
227
|
+
return { available: false, message: "That name is trademarked." };
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
}, adapter);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Validators run sequentially and stop at the first rejection. They receive the normalized identifier.
|
|
236
|
+
|
|
237
|
+
## Conflict Suggestions
|
|
238
|
+
|
|
239
|
+
When a slug is taken, automatically suggest available alternatives:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const guard = createNamespaceGuard({
|
|
243
|
+
sources: [/* ... */],
|
|
244
|
+
suggest: {
|
|
245
|
+
// Optional: custom generator (default appends -1 through -9)
|
|
246
|
+
generate: (identifier) => [
|
|
247
|
+
`${identifier}-1`,
|
|
248
|
+
`${identifier}-2`,
|
|
249
|
+
`${identifier}-io`,
|
|
250
|
+
`${identifier}-app`,
|
|
251
|
+
`${identifier}-hq`,
|
|
252
|
+
],
|
|
253
|
+
// Optional: max suggestions to return (default: 3)
|
|
254
|
+
max: 3,
|
|
255
|
+
},
|
|
256
|
+
}, adapter);
|
|
257
|
+
|
|
258
|
+
const result = await guard.check("acme-corp");
|
|
259
|
+
// {
|
|
260
|
+
// available: false,
|
|
261
|
+
// reason: "taken",
|
|
262
|
+
// message: "That name is already in use.",
|
|
263
|
+
// source: "organization",
|
|
264
|
+
// suggestions: ["acme-corp-1", "acme-corp-2", "acme-corp-io"]
|
|
265
|
+
// }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Suggestions are verified against format, reserved names, and database collisions. Only available suggestions are returned.
|
|
269
|
+
|
|
270
|
+
## Batch Checking
|
|
271
|
+
|
|
272
|
+
Check multiple identifiers at once:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const results = await guard.checkMany(["sarah", "admin", "acme-corp"]);
|
|
276
|
+
// {
|
|
277
|
+
// sarah: { available: true },
|
|
278
|
+
// admin: { available: false, reason: "reserved", ... },
|
|
279
|
+
// "acme-corp": { available: false, reason: "taken", ... }
|
|
280
|
+
// }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
All checks run in parallel. Accepts an optional scope parameter.
|
|
284
|
+
|
|
285
|
+
## Ownership Scoping
|
|
286
|
+
|
|
287
|
+
When users update their own slug, you don't want a false "already taken" error:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// User with ID "user_123" wants to change handle from "sarah" to "sarah-dev"
|
|
291
|
+
// Without scoping, this would error because "sarah-dev" != their current handle
|
|
292
|
+
|
|
293
|
+
// Pass their ID to exclude their own record from collision detection
|
|
294
|
+
const result = await guard.check("sarah-dev", { id: "user_123" });
|
|
295
|
+
// Available (unless another user/org has it)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The scope object keys map to `scopeKey` in your source config. This lets you check multiple ownership types:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Check if a user OR their org owns this slug
|
|
302
|
+
const result = await guard.check("acme", {
|
|
303
|
+
userId: currentUser.id,
|
|
304
|
+
orgId: currentOrg.id,
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## CLI
|
|
309
|
+
|
|
310
|
+
Validate slugs from the command line:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Format + reserved name checking (no database needed)
|
|
314
|
+
npx namespace-guard check acme-corp
|
|
315
|
+
# ✓ acme-corp is available
|
|
316
|
+
|
|
317
|
+
npx namespace-guard check admin
|
|
318
|
+
# ✗ admin — That name is reserved. Try another one.
|
|
319
|
+
|
|
320
|
+
npx namespace-guard check "a"
|
|
321
|
+
# ✗ a — Use 2-30 lowercase letters, numbers, or hyphens.
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### With a config file
|
|
325
|
+
|
|
326
|
+
Create `namespace-guard.config.json`:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"reserved": ["admin", "api", "settings", "dashboard"],
|
|
331
|
+
"pattern": "^[a-z0-9][a-z0-9-]{2,39}$",
|
|
332
|
+
"sources": [
|
|
333
|
+
{ "name": "users", "column": "handle" },
|
|
334
|
+
{ "name": "organizations", "column": "slug" }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Or with categorized reserved names:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"reserved": {
|
|
344
|
+
"system": ["admin", "api", "settings"],
|
|
345
|
+
"brand": ["oncor"]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
npx namespace-guard check sarah --config ./my-config.json
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### With database checking
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npx namespace-guard check sarah --database-url postgres://localhost/mydb
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Requires `pg` to be installed (`npm install pg`).
|
|
361
|
+
|
|
362
|
+
Exit code 0 = available, 1 = unavailable.
|
|
363
|
+
|
|
364
|
+
## API Reference
|
|
365
|
+
|
|
366
|
+
### `createNamespaceGuard(config, adapter)`
|
|
367
|
+
|
|
368
|
+
Creates a guard instance with your configuration and database adapter.
|
|
369
|
+
|
|
370
|
+
**Returns:** `NamespaceGuard` instance
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### `guard.check(identifier, scope?)`
|
|
375
|
+
|
|
376
|
+
Check if an identifier is available.
|
|
377
|
+
|
|
378
|
+
**Parameters:**
|
|
379
|
+
- `identifier` - The slug/handle to check
|
|
380
|
+
- `scope` - Optional ownership scope to exclude own records
|
|
381
|
+
|
|
382
|
+
**Returns:**
|
|
383
|
+
```typescript
|
|
384
|
+
// Available
|
|
385
|
+
{ available: true }
|
|
386
|
+
|
|
387
|
+
// Not available
|
|
388
|
+
{
|
|
389
|
+
available: false,
|
|
390
|
+
reason: "invalid" | "reserved" | "taken",
|
|
391
|
+
message: string,
|
|
392
|
+
source?: string, // Which table caused the collision (reason: "taken")
|
|
393
|
+
category?: string, // Reserved name category (reason: "reserved")
|
|
394
|
+
suggestions?: string[] // Available alternatives (reason: "taken", requires suggest config)
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### `guard.checkMany(identifiers, scope?)`
|
|
401
|
+
|
|
402
|
+
Check multiple identifiers in parallel.
|
|
403
|
+
|
|
404
|
+
**Returns:** `Record<string, CheckResult>`
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### `guard.assertAvailable(identifier, scope?)`
|
|
409
|
+
|
|
410
|
+
Same as `check()`, but throws an `Error` if not available.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### `guard.validateFormat(identifier)`
|
|
415
|
+
|
|
416
|
+
Validate format only (no database queries).
|
|
417
|
+
|
|
418
|
+
**Returns:** Error message string if invalid, `null` if valid.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
### `normalize(identifier)`
|
|
423
|
+
|
|
424
|
+
Utility function to normalize identifiers. Trims whitespace, lowercases, and strips leading `@` symbols.
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { normalize } from "namespace-guard";
|
|
428
|
+
|
|
429
|
+
normalize(" @Sarah "); // "sarah"
|
|
430
|
+
normalize("ACME-Corp"); // "acme-corp"
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Real-World Example
|
|
434
|
+
|
|
435
|
+
Here's how you might use this in a signup flow:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// In your API route / server action
|
|
439
|
+
async function createUser(handle: string, email: string) {
|
|
440
|
+
// Normalize first
|
|
441
|
+
const normalizedHandle = guard.normalize(handle);
|
|
442
|
+
|
|
443
|
+
// Check availability (will also validate format)
|
|
444
|
+
const result = await guard.check(normalizedHandle);
|
|
445
|
+
|
|
446
|
+
if (!result.available) {
|
|
447
|
+
return { error: result.message };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Safe to create
|
|
451
|
+
const user = await prisma.user.create({
|
|
452
|
+
data: { handle: normalizedHandle, email },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return { user };
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Or in an update flow with ownership scoping:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
async function updateUserHandle(userId: string, newHandle: string) {
|
|
463
|
+
const normalized = guard.normalize(newHandle);
|
|
464
|
+
|
|
465
|
+
// Pass userId to avoid collision with own current handle
|
|
466
|
+
const result = await guard.check(normalized, { id: userId });
|
|
467
|
+
|
|
468
|
+
if (!result.available) {
|
|
469
|
+
return { error: result.message };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
await prisma.user.update({
|
|
473
|
+
where: { id: userId },
|
|
474
|
+
data: { handle: normalized },
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return { success: true };
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## TypeScript
|
|
482
|
+
|
|
483
|
+
Full TypeScript support with exported types:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import type {
|
|
487
|
+
NamespaceConfig,
|
|
488
|
+
NamespaceSource,
|
|
489
|
+
NamespaceAdapter,
|
|
490
|
+
NamespaceGuard,
|
|
491
|
+
CheckResult,
|
|
492
|
+
OwnershipScope,
|
|
493
|
+
} from "namespace-guard";
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Contributing
|
|
497
|
+
|
|
498
|
+
Contributions welcome! Please open an issue first to discuss what you'd like to change.
|
|
499
|
+
|
|
500
|
+
## License
|
|
501
|
+
|
|
502
|
+
MIT © [Paul Wood FRSA](https://github.com/paultendo)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NamespaceAdapter } from '../index.mjs';
|
|
2
|
+
|
|
3
|
+
type DrizzleTable = {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type DrizzleDb = {
|
|
7
|
+
query: {
|
|
8
|
+
[key: string]: {
|
|
9
|
+
findFirst: (args: {
|
|
10
|
+
where: unknown;
|
|
11
|
+
columns?: Record<string, boolean>;
|
|
12
|
+
}) => Promise<Record<string, unknown> | null>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type EqFn = (column: unknown, value: unknown) => unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Create a namespace adapter for Drizzle ORM
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { eq } from "drizzle-orm";
|
|
23
|
+
* import { db } from "./db";
|
|
24
|
+
* import { users, organizations } from "./schema";
|
|
25
|
+
* import { createNamespaceGuard } from "namespace-guard";
|
|
26
|
+
* import { createDrizzleAdapter } from "namespace-guard/adapters/drizzle";
|
|
27
|
+
*
|
|
28
|
+
* const guard = createNamespaceGuard(
|
|
29
|
+
* {
|
|
30
|
+
* reserved: ["admin", "api", "settings"],
|
|
31
|
+
* sources: [
|
|
32
|
+
* { name: "users", column: "handle", scopeKey: "id" },
|
|
33
|
+
* { name: "organizations", column: "slug", scopeKey: "id" },
|
|
34
|
+
* ],
|
|
35
|
+
* },
|
|
36
|
+
* createDrizzleAdapter(db, { users, organizations }, eq)
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function createDrizzleAdapter(db: DrizzleDb, tables: Record<string, DrizzleTable>, eq: EqFn): NamespaceAdapter;
|
|
41
|
+
|
|
42
|
+
export { createDrizzleAdapter };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NamespaceAdapter } from '../index.js';
|
|
2
|
+
|
|
3
|
+
type DrizzleTable = {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
type DrizzleDb = {
|
|
7
|
+
query: {
|
|
8
|
+
[key: string]: {
|
|
9
|
+
findFirst: (args: {
|
|
10
|
+
where: unknown;
|
|
11
|
+
columns?: Record<string, boolean>;
|
|
12
|
+
}) => Promise<Record<string, unknown> | null>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type EqFn = (column: unknown, value: unknown) => unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Create a namespace adapter for Drizzle ORM
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { eq } from "drizzle-orm";
|
|
23
|
+
* import { db } from "./db";
|
|
24
|
+
* import { users, organizations } from "./schema";
|
|
25
|
+
* import { createNamespaceGuard } from "namespace-guard";
|
|
26
|
+
* import { createDrizzleAdapter } from "namespace-guard/adapters/drizzle";
|
|
27
|
+
*
|
|
28
|
+
* const guard = createNamespaceGuard(
|
|
29
|
+
* {
|
|
30
|
+
* reserved: ["admin", "api", "settings"],
|
|
31
|
+
* sources: [
|
|
32
|
+
* { name: "users", column: "handle", scopeKey: "id" },
|
|
33
|
+
* { name: "organizations", column: "slug", scopeKey: "id" },
|
|
34
|
+
* ],
|
|
35
|
+
* },
|
|
36
|
+
* createDrizzleAdapter(db, { users, organizations }, eq)
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function createDrizzleAdapter(db: DrizzleDb, tables: Record<string, DrizzleTable>, eq: EqFn): NamespaceAdapter;
|
|
41
|
+
|
|
42
|
+
export { createDrizzleAdapter };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/drizzle.ts
|
|
21
|
+
var drizzle_exports = {};
|
|
22
|
+
__export(drizzle_exports, {
|
|
23
|
+
createDrizzleAdapter: () => createDrizzleAdapter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(drizzle_exports);
|
|
26
|
+
function createDrizzleAdapter(db, tables, eq) {
|
|
27
|
+
return {
|
|
28
|
+
async findOne(source, value) {
|
|
29
|
+
const queryHandler = db.query[source.name];
|
|
30
|
+
if (!queryHandler) {
|
|
31
|
+
throw new Error(`Drizzle query handler for "${source.name}" not found. Make sure relational queries are set up.`);
|
|
32
|
+
}
|
|
33
|
+
const table = tables[source.name];
|
|
34
|
+
if (!table) {
|
|
35
|
+
throw new Error(`Table "${source.name}" not found in provided tables object`);
|
|
36
|
+
}
|
|
37
|
+
const column = table[source.column];
|
|
38
|
+
if (!column) {
|
|
39
|
+
throw new Error(`Column "${source.column}" not found in table "${source.name}"`);
|
|
40
|
+
}
|
|
41
|
+
const idColumn = source.idColumn ?? "id";
|
|
42
|
+
return queryHandler.findFirst({
|
|
43
|
+
where: eq(column, value),
|
|
44
|
+
columns: {
|
|
45
|
+
[idColumn]: true,
|
|
46
|
+
...source.scopeKey && source.scopeKey !== idColumn ? { [source.scopeKey]: true } : {}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
53
|
+
0 && (module.exports = {
|
|
54
|
+
createDrizzleAdapter
|
|
55
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/adapters/drizzle.ts
|
|
2
|
+
function createDrizzleAdapter(db, tables, eq) {
|
|
3
|
+
return {
|
|
4
|
+
async findOne(source, value) {
|
|
5
|
+
const queryHandler = db.query[source.name];
|
|
6
|
+
if (!queryHandler) {
|
|
7
|
+
throw new Error(`Drizzle query handler for "${source.name}" not found. Make sure relational queries are set up.`);
|
|
8
|
+
}
|
|
9
|
+
const table = tables[source.name];
|
|
10
|
+
if (!table) {
|
|
11
|
+
throw new Error(`Table "${source.name}" not found in provided tables object`);
|
|
12
|
+
}
|
|
13
|
+
const column = table[source.column];
|
|
14
|
+
if (!column) {
|
|
15
|
+
throw new Error(`Column "${source.column}" not found in table "${source.name}"`);
|
|
16
|
+
}
|
|
17
|
+
const idColumn = source.idColumn ?? "id";
|
|
18
|
+
return queryHandler.findFirst({
|
|
19
|
+
where: eq(column, value),
|
|
20
|
+
columns: {
|
|
21
|
+
[idColumn]: true,
|
|
22
|
+
...source.scopeKey && source.scopeKey !== idColumn ? { [source.scopeKey]: true } : {}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
createDrizzleAdapter
|
|
30
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NamespaceAdapter } from '../index.mjs';
|
|
2
|
+
|
|
3
|
+
type KnexQueryBuilder = {
|
|
4
|
+
select: (columns: string[]) => KnexQueryBuilder;
|
|
5
|
+
where: (column: string, value: unknown) => KnexQueryBuilder;
|
|
6
|
+
first: () => Promise<Record<string, unknown> | undefined>;
|
|
7
|
+
};
|
|
8
|
+
type KnexInstance = {
|
|
9
|
+
(tableName: string): KnexQueryBuilder;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Create a namespace adapter for Knex
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import Knex from "knex";
|
|
17
|
+
* import { createNamespaceGuard } from "namespace-guard";
|
|
18
|
+
* import { createKnexAdapter } from "namespace-guard/adapters/knex";
|
|
19
|
+
*
|
|
20
|
+
* const knex = Knex({ client: "pg", connection: process.env.DATABASE_URL });
|
|
21
|
+
*
|
|
22
|
+
* const guard = createNamespaceGuard(
|
|
23
|
+
* {
|
|
24
|
+
* reserved: ["admin", "api", "settings"],
|
|
25
|
+
* sources: [
|
|
26
|
+
* { name: "users", column: "handle", scopeKey: "id" },
|
|
27
|
+
* { name: "organizations", column: "slug", scopeKey: "id" },
|
|
28
|
+
* ],
|
|
29
|
+
* },
|
|
30
|
+
* createKnexAdapter(knex)
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function createKnexAdapter(knex: KnexInstance): NamespaceAdapter;
|
|
35
|
+
|
|
36
|
+
export { createKnexAdapter };
|