namespace-guard 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -92,6 +92,8 @@ const adapter = createPrismaAdapter(prisma);
92
92
 
93
93
  ### Drizzle
94
94
 
95
+ > **Note:** The Drizzle adapter uses `db.query` (the relational query API). Make sure your Drizzle client is set up with `drizzle(client, { schema })` so that `db.query.<tableName>` is available.
96
+
95
97
  ```typescript
96
98
  import { eq } from "drizzle-orm";
97
99
  import { createDrizzleAdapter } from "namespace-guard/adapters/drizzle";
@@ -163,7 +165,9 @@ import { User, Organization } from "./models";
163
165
  const adapter = createMongooseAdapter({ user: User, organization: Organization });
164
166
  ```
165
167
 
166
- ### Raw SQL (pg, mysql2, etc.)
168
+ ### Raw SQL (pg, mysql2, better-sqlite3, etc.)
169
+
170
+ The raw adapter generates PostgreSQL-style SQL (`$1` placeholders, double-quoted identifiers). For pg this works directly. For MySQL or SQLite, translate the parameter syntax in your executor wrapper.
167
171
 
168
172
  ```typescript
169
173
  import { Pool } from "pg";
@@ -173,6 +177,34 @@ const pool = new Pool();
173
177
  const adapter = createRawAdapter((sql, params) => pool.query(sql, params));
174
178
  ```
175
179
 
180
+ **MySQL2 wrapper** (translates `$1` to `?` and `"col"` to `` `col` ``):
181
+
182
+ ```typescript
183
+ import mysql from "mysql2/promise";
184
+ import { createRawAdapter } from "namespace-guard/adapters/raw";
185
+
186
+ const pool = mysql.createPool({ uri: process.env.DATABASE_URL });
187
+ const adapter = createRawAdapter(async (sql, params) => {
188
+ const mysqlSql = sql.replace(/\$\d+/g, "?").replace(/"/g, "`");
189
+ const [rows] = await pool.execute(mysqlSql, params);
190
+ return { rows: rows as Record<string, unknown>[] };
191
+ });
192
+ ```
193
+
194
+ **better-sqlite3 wrapper** (translates `$1` to `?` and strips identifier quotes):
195
+
196
+ ```typescript
197
+ import Database from "better-sqlite3";
198
+ import { createRawAdapter } from "namespace-guard/adapters/raw";
199
+
200
+ const db = new Database("app.db");
201
+ const adapter = createRawAdapter(async (sql, params) => {
202
+ const sqliteSql = sql.replace(/\$\d+/g, "?").replace(/"/g, "");
203
+ const rows = db.prepare(sqliteSql).all(...params);
204
+ return { rows: rows as Record<string, unknown>[] };
205
+ });
206
+ ```
207
+
176
208
  ## Configuration
177
209
 
178
210
  ```typescript
@@ -623,9 +655,16 @@ Check if an identifier is available.
623
655
 
624
656
  ---
625
657
 
626
- ### `guard.checkMany(identifiers, scope?)`
658
+ ### `guard.checkMany(identifiers, scope?, options?)`
659
+
660
+ Check multiple identifiers in parallel. Suggestions are skipped by default for performance.
661
+
662
+ **Parameters:**
663
+ - `identifiers` - Array of slugs/handles to check
664
+ - `scope` - Optional ownership scope applied to all checks
665
+ - `options` - Optional `{ skipSuggestions?: boolean }` (default: `true`)
627
666
 
628
- Check multiple identifiers in parallel.
667
+ Pass `{ skipSuggestions: false }` to include suggestions for taken identifiers.
629
668
 
630
669
  **Returns:** `Record<string, CheckResult>`
631
670
 
@@ -639,9 +678,37 @@ Same as `check()`, but throws an `Error` if not available.
639
678
 
640
679
  ### `guard.validateFormat(identifier)`
641
680
 
642
- Validate format only (no database queries).
681
+ Validate format, purely-numeric restriction, and reserved name status without querying the database.
682
+
683
+ **Returns:** Error message string if invalid or reserved, `null` if OK.
684
+
685
+ ---
686
+
687
+ ### `guard.validateFormatOnly(identifier)`
688
+
689
+ Validate only the identifier's format and purely-numeric restriction. Does not check reserved names or query the database. Useful for instant client-side feedback on input shape.
690
+
691
+ **Returns:** Error message string if the format is invalid, `null` if OK.
692
+
693
+ ---
694
+
695
+ ### `guard.normalize(identifier)`
696
+
697
+ Convenience re-export of the standalone `normalize()` function. Note: always applies NFKC normalization regardless of the guard's `normalizeUnicode` setting. Use `normalize(id, { unicode: false })` directly if you need to skip NFKC.
698
+
699
+ ---
700
+
701
+ ### `guard.clearCache()`
702
+
703
+ Clear the in-memory cache and reset hit/miss counters. No-op if caching is not enabled.
704
+
705
+ ---
706
+
707
+ ### `guard.cacheStats()`
708
+
709
+ Get cache performance statistics.
643
710
 
644
- **Returns:** Error message string if invalid, `null` if valid.
711
+ **Returns:** `{ size: number; hits: number; misses: number }`
645
712
 
646
713
  ---
647
714
 
@@ -813,6 +880,7 @@ import {
813
880
  type OwnershipScope,
814
881
  type SuggestStrategyName,
815
882
  type SkeletonOptions,
883
+ type CheckManyOptions,
816
884
  } from "namespace-guard";
817
885
  ```
818
886
 
package/dist/cli.js CHANGED
@@ -339,6 +339,16 @@ function createNamespaceGuard(config, adapter) {
339
339
  }
340
340
  return null;
341
341
  }
342
+ function validateFormatOnly(identifier) {
343
+ const normalized = normalize(identifier, normalizeOpts);
344
+ if (!pattern.test(normalized)) {
345
+ return invalidMsg;
346
+ }
347
+ if (!allowPurelyNumeric && /^\d+(-\d+)*$/.test(normalized)) {
348
+ return purelyNumericMsg;
349
+ }
350
+ return null;
351
+ }
342
352
  function isOwnedByScope(existing, source, scope) {
343
353
  if (!source.scopeKey) return false;
344
354
  const scopeValue = scope[source.scopeKey];
@@ -457,10 +467,11 @@ function createNamespaceGuard(config, adapter) {
457
467
  throw new Error(result.message);
458
468
  }
459
469
  }
460
- async function checkMany(identifiers, scope = {}) {
470
+ async function checkMany(identifiers, scope = {}, options) {
471
+ const skip = options?.skipSuggestions ?? true;
461
472
  const entries = await Promise.all(
462
473
  identifiers.map(async (id) => {
463
- const result = await check(id, scope, { skipSuggestions: true });
474
+ const result = await check(id, scope, { skipSuggestions: skip });
464
475
  return [id, result];
465
476
  })
466
477
  );
@@ -477,6 +488,7 @@ function createNamespaceGuard(config, adapter) {
477
488
  return {
478
489
  normalize,
479
490
  validateFormat,
491
+ validateFormatOnly,
480
492
  check,
481
493
  assertAvailable,
482
494
  checkMany,
package/dist/cli.mjs CHANGED
@@ -316,6 +316,16 @@ function createNamespaceGuard(config, adapter) {
316
316
  }
317
317
  return null;
318
318
  }
319
+ function validateFormatOnly(identifier) {
320
+ const normalized = normalize(identifier, normalizeOpts);
321
+ if (!pattern.test(normalized)) {
322
+ return invalidMsg;
323
+ }
324
+ if (!allowPurelyNumeric && /^\d+(-\d+)*$/.test(normalized)) {
325
+ return purelyNumericMsg;
326
+ }
327
+ return null;
328
+ }
319
329
  function isOwnedByScope(existing, source, scope) {
320
330
  if (!source.scopeKey) return false;
321
331
  const scopeValue = scope[source.scopeKey];
@@ -434,10 +444,11 @@ function createNamespaceGuard(config, adapter) {
434
444
  throw new Error(result.message);
435
445
  }
436
446
  }
437
- async function checkMany(identifiers, scope = {}) {
447
+ async function checkMany(identifiers, scope = {}, options) {
448
+ const skip = options?.skipSuggestions ?? true;
438
449
  const entries = await Promise.all(
439
450
  identifiers.map(async (id) => {
440
- const result = await check(id, scope, { skipSuggestions: true });
451
+ const result = await check(id, scope, { skipSuggestions: skip });
441
452
  return [id, result];
442
453
  })
443
454
  );
@@ -454,6 +465,7 @@ function createNamespaceGuard(config, adapter) {
454
465
  return {
455
466
  normalize,
456
467
  validateFormat,
468
+ validateFormatOnly,
457
469
  check,
458
470
  assertAvailable,
459
471
  checkMany,
package/dist/index.d.mts CHANGED
@@ -82,6 +82,11 @@ type CheckResult = {
82
82
  category?: string;
83
83
  suggestions?: string[];
84
84
  };
85
+ /** Options for `checkMany()`. */
86
+ type CheckManyOptions = {
87
+ /** Skip suggestion generation for taken identifiers (default: `true`). */
88
+ skipSuggestions?: boolean;
89
+ };
85
90
  /** Options for the `skeleton()` and `areConfusable()` functions. */
86
91
  type SkeletonOptions = {
87
92
  /** Confusable character map to use.
@@ -250,7 +255,7 @@ declare function areConfusable(a: string, b: string, options?: SkeletonOptions):
250
255
  *
251
256
  * @param config - Reserved names, data sources, validation pattern, and optional features
252
257
  * @param adapter - Database adapter implementing the `findOne` lookup (use a built-in adapter or write your own)
253
- * @returns A guard with `check`, `checkMany`, `assertAvailable`, `validateFormat`, `clearCache`, and `cacheStats` methods
258
+ * @returns A guard with `check`, `checkMany`, `assertAvailable`, `validateFormat`, `validateFormatOnly`, `clearCache`, and `cacheStats` methods
254
259
  *
255
260
  * @example
256
261
  * ```ts
@@ -277,11 +282,12 @@ declare function areConfusable(a: string, b: string, options?: SkeletonOptions):
277
282
  declare function createNamespaceGuard(config: NamespaceConfig, adapter: NamespaceAdapter): {
278
283
  normalize: typeof normalize;
279
284
  validateFormat: (identifier: string) => string | null;
285
+ validateFormatOnly: (identifier: string) => string | null;
280
286
  check: (identifier: string, scope?: OwnershipScope, options?: {
281
287
  skipSuggestions?: boolean;
282
288
  }) => Promise<CheckResult>;
283
289
  assertAvailable: (identifier: string, scope?: OwnershipScope) => Promise<void>;
284
- checkMany: (identifiers: string[], scope?: OwnershipScope) => Promise<Record<string, CheckResult>>;
290
+ checkMany: (identifiers: string[], scope?: OwnershipScope, options?: CheckManyOptions) => Promise<Record<string, CheckResult>>;
285
291
  clearCache: () => void;
286
292
  cacheStats: () => {
287
293
  size: number;
@@ -292,4 +298,4 @@ declare function createNamespaceGuard(config: NamespaceConfig, adapter: Namespac
292
298
  /** The guard instance returned by `createNamespaceGuard`. */
293
299
  type NamespaceGuard = ReturnType<typeof createNamespaceGuard>;
294
300
 
295
- export { CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CheckResult, type FindOneOptions, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceSource, type OwnershipScope, type SkeletonOptions, type SuggestStrategyName, areConfusable, createHomoglyphValidator, createNamespaceGuard, createProfanityValidator, normalize, skeleton };
301
+ export { CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CheckManyOptions, type CheckResult, type FindOneOptions, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceSource, type OwnershipScope, type SkeletonOptions, type SuggestStrategyName, areConfusable, createHomoglyphValidator, createNamespaceGuard, createProfanityValidator, normalize, skeleton };
package/dist/index.d.ts CHANGED
@@ -82,6 +82,11 @@ type CheckResult = {
82
82
  category?: string;
83
83
  suggestions?: string[];
84
84
  };
85
+ /** Options for `checkMany()`. */
86
+ type CheckManyOptions = {
87
+ /** Skip suggestion generation for taken identifiers (default: `true`). */
88
+ skipSuggestions?: boolean;
89
+ };
85
90
  /** Options for the `skeleton()` and `areConfusable()` functions. */
86
91
  type SkeletonOptions = {
87
92
  /** Confusable character map to use.
@@ -250,7 +255,7 @@ declare function areConfusable(a: string, b: string, options?: SkeletonOptions):
250
255
  *
251
256
  * @param config - Reserved names, data sources, validation pattern, and optional features
252
257
  * @param adapter - Database adapter implementing the `findOne` lookup (use a built-in adapter or write your own)
253
- * @returns A guard with `check`, `checkMany`, `assertAvailable`, `validateFormat`, `clearCache`, and `cacheStats` methods
258
+ * @returns A guard with `check`, `checkMany`, `assertAvailable`, `validateFormat`, `validateFormatOnly`, `clearCache`, and `cacheStats` methods
254
259
  *
255
260
  * @example
256
261
  * ```ts
@@ -277,11 +282,12 @@ declare function areConfusable(a: string, b: string, options?: SkeletonOptions):
277
282
  declare function createNamespaceGuard(config: NamespaceConfig, adapter: NamespaceAdapter): {
278
283
  normalize: typeof normalize;
279
284
  validateFormat: (identifier: string) => string | null;
285
+ validateFormatOnly: (identifier: string) => string | null;
280
286
  check: (identifier: string, scope?: OwnershipScope, options?: {
281
287
  skipSuggestions?: boolean;
282
288
  }) => Promise<CheckResult>;
283
289
  assertAvailable: (identifier: string, scope?: OwnershipScope) => Promise<void>;
284
- checkMany: (identifiers: string[], scope?: OwnershipScope) => Promise<Record<string, CheckResult>>;
290
+ checkMany: (identifiers: string[], scope?: OwnershipScope, options?: CheckManyOptions) => Promise<Record<string, CheckResult>>;
285
291
  clearCache: () => void;
286
292
  cacheStats: () => {
287
293
  size: number;
@@ -292,4 +298,4 @@ declare function createNamespaceGuard(config: NamespaceConfig, adapter: Namespac
292
298
  /** The guard instance returned by `createNamespaceGuard`. */
293
299
  type NamespaceGuard = ReturnType<typeof createNamespaceGuard>;
294
300
 
295
- export { CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CheckResult, type FindOneOptions, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceSource, type OwnershipScope, type SkeletonOptions, type SuggestStrategyName, areConfusable, createHomoglyphValidator, createNamespaceGuard, createProfanityValidator, normalize, skeleton };
301
+ export { CONFUSABLE_MAP, CONFUSABLE_MAP_FULL, type CheckManyOptions, type CheckResult, type FindOneOptions, type NamespaceAdapter, type NamespaceConfig, type NamespaceGuard, type NamespaceSource, type OwnershipScope, type SkeletonOptions, type SuggestStrategyName, areConfusable, createHomoglyphValidator, createNamespaceGuard, createProfanityValidator, normalize, skeleton };
package/dist/index.js CHANGED
@@ -2837,6 +2837,16 @@ function createNamespaceGuard(config, adapter) {
2837
2837
  }
2838
2838
  return null;
2839
2839
  }
2840
+ function validateFormatOnly(identifier) {
2841
+ const normalized = normalize(identifier, normalizeOpts);
2842
+ if (!pattern.test(normalized)) {
2843
+ return invalidMsg;
2844
+ }
2845
+ if (!allowPurelyNumeric && /^\d+(-\d+)*$/.test(normalized)) {
2846
+ return purelyNumericMsg;
2847
+ }
2848
+ return null;
2849
+ }
2840
2850
  function isOwnedByScope(existing, source, scope) {
2841
2851
  if (!source.scopeKey) return false;
2842
2852
  const scopeValue = scope[source.scopeKey];
@@ -2955,10 +2965,11 @@ function createNamespaceGuard(config, adapter) {
2955
2965
  throw new Error(result.message);
2956
2966
  }
2957
2967
  }
2958
- async function checkMany(identifiers, scope = {}) {
2968
+ async function checkMany(identifiers, scope = {}, options) {
2969
+ const skip = options?.skipSuggestions ?? true;
2959
2970
  const entries = await Promise.all(
2960
2971
  identifiers.map(async (id) => {
2961
- const result = await check(id, scope, { skipSuggestions: true });
2972
+ const result = await check(id, scope, { skipSuggestions: skip });
2962
2973
  return [id, result];
2963
2974
  })
2964
2975
  );
@@ -2975,6 +2986,7 @@ function createNamespaceGuard(config, adapter) {
2975
2986
  return {
2976
2987
  normalize,
2977
2988
  validateFormat,
2989
+ validateFormatOnly,
2978
2990
  check,
2979
2991
  assertAvailable,
2980
2992
  checkMany,
package/dist/index.mjs CHANGED
@@ -2806,6 +2806,16 @@ function createNamespaceGuard(config, adapter) {
2806
2806
  }
2807
2807
  return null;
2808
2808
  }
2809
+ function validateFormatOnly(identifier) {
2810
+ const normalized = normalize(identifier, normalizeOpts);
2811
+ if (!pattern.test(normalized)) {
2812
+ return invalidMsg;
2813
+ }
2814
+ if (!allowPurelyNumeric && /^\d+(-\d+)*$/.test(normalized)) {
2815
+ return purelyNumericMsg;
2816
+ }
2817
+ return null;
2818
+ }
2809
2819
  function isOwnedByScope(existing, source, scope) {
2810
2820
  if (!source.scopeKey) return false;
2811
2821
  const scopeValue = scope[source.scopeKey];
@@ -2924,10 +2934,11 @@ function createNamespaceGuard(config, adapter) {
2924
2934
  throw new Error(result.message);
2925
2935
  }
2926
2936
  }
2927
- async function checkMany(identifiers, scope = {}) {
2937
+ async function checkMany(identifiers, scope = {}, options) {
2938
+ const skip = options?.skipSuggestions ?? true;
2928
2939
  const entries = await Promise.all(
2929
2940
  identifiers.map(async (id) => {
2930
- const result = await check(id, scope, { skipSuggestions: true });
2941
+ const result = await check(id, scope, { skipSuggestions: skip });
2931
2942
  return [id, result];
2932
2943
  })
2933
2944
  );
@@ -2944,6 +2955,7 @@ function createNamespaceGuard(config, adapter) {
2944
2955
  return {
2945
2956
  normalize,
2946
2957
  validateFormat,
2958
+ validateFormatOnly,
2947
2959
  check,
2948
2960
  assertAvailable,
2949
2961
  checkMany,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "namespace-guard",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Check slug/handle uniqueness across multiple database tables with reserved name protection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -88,7 +88,20 @@
88
88
  "sequelize",
89
89
  "mongoose",
90
90
  "mongodb",
91
- "cli"
91
+ "cli",
92
+ "homoglyph",
93
+ "confusable",
94
+ "unicode",
95
+ "anti-spoofing",
96
+ "reserved-usernames",
97
+ "slug-validation",
98
+ "security",
99
+ "nodejs",
100
+ "profanity-filter",
101
+ "skeleton",
102
+ "tr39",
103
+ "nfkc",
104
+ "typescript"
92
105
  ],
93
106
  "author": "Paul Wood FRSA",
94
107
  "license": "MIT",