@everystack/api 0.2.12 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/api",
3
- "version": "0.2.12",
3
+ "version": "0.4.0",
4
4
  "description": "PostgREST-compatible HTTP handler + typed fetch client for Drizzle ORM",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -522,8 +522,81 @@ function transformBodyKeys(body: Record<string, unknown>, table: DrizzleTable):
522
522
  return result;
523
523
  }
524
524
 
525
- export function createHandler(db: DrizzleDb, schema: Schema, options?: HandlerOptions): Handler {
525
+ /** The name a drizzle pgTable knows itself by in SQL, or null for a non-table export. */
526
+ function sqlNameOf(value: unknown): string | null {
527
+ if (!value || typeof value !== 'object') return null;
528
+ const name = (value as Record<symbol, unknown>)[Symbol.for('drizzle:Name')];
529
+ return typeof name === 'string' ? name : null;
530
+ }
531
+
532
+ /** Export-key ↔ SQL-name pairs for every table whose two spellings differ. */
533
+ function tableNamePairs(schema: Schema): Array<{ exportKey: string; sqlName: string }> {
534
+ const pairs: Array<{ exportKey: string; sqlName: string }> = [];
535
+ for (const [exportKey, value] of Object.entries(schema)) {
536
+ const sqlName = sqlNameOf(value);
537
+ if (sqlName && sqlName !== exportKey) pairs.push({ exportKey, sqlName });
538
+ }
539
+ return pairs;
540
+ }
541
+
542
+ /** The schema with each table reachable under BOTH spellings (existing keys win). */
543
+ function expandSchema(schema: Schema, pairs: Array<{ exportKey: string; sqlName: string }>): Schema {
544
+ if (pairs.length === 0) return schema;
545
+ const expanded: Schema = { ...schema };
546
+ for (const { exportKey, sqlName } of pairs) {
547
+ if (!(sqlName in expanded)) expanded[sqlName] = schema[exportKey];
548
+ }
549
+ return expanded;
550
+ }
551
+
552
+ /**
553
+ * Table-keyed options written in EITHER naming domain apply to both spellings.
554
+ * `deriveHandlerConfig` emits SQL names; hand-written configs use export keys —
555
+ * each key gains its twin (same value reference), so neither authoring style
556
+ * can strand a table behind the other domain's gate.
557
+ */
558
+ function expandTableKeyedOptions(
559
+ options: HandlerOptions | undefined,
560
+ pairs: Array<{ exportKey: string; sqlName: string }>,
561
+ ): HandlerOptions | undefined {
562
+ if (!options || pairs.length === 0) return options;
563
+ const twin = (name: string): string | undefined =>
564
+ pairs.find((p) => p.exportKey === name)?.sqlName ?? pairs.find((p) => p.sqlName === name)?.exportKey;
565
+ const expandList = (list: string[]): string[] =>
566
+ [...new Set(list.flatMap((name) => { const t = twin(name); return t ? [name, t] : [name]; }))];
567
+ const expandMap = <T,>(map: Record<string, T>): Record<string, T> => {
568
+ const out: Record<string, T> = { ...map };
569
+ for (const key of Object.keys(map)) {
570
+ const t = twin(key);
571
+ if (t && !(t in out)) out[t] = map[key];
572
+ }
573
+ return out;
574
+ };
575
+ return {
576
+ ...options,
577
+ ...(options.exposedTables ? { exposedTables: expandList(options.exposedTables) } : {}),
578
+ ...(options.hiddenColumns ? { hiddenColumns: expandMap(options.hiddenColumns) } : {}),
579
+ ...(options.protectedFields ? { protectedFields: expandMap(options.protectedFields) } : {}),
580
+ ...(options.rowOwnership ? { rowOwnership: expandMap(options.rowOwnership) } : {}),
581
+ ...(options.relations ? { relations: expandMap(options.relations) } : {}),
582
+ ...(options.hooks ? { hooks: expandMap(options.hooks) } : {}),
583
+ ...(options.softDelete?.tables
584
+ ? { softDelete: { ...options.softDelete, tables: expandList(options.softDelete.tables) } }
585
+ : {}),
586
+ };
587
+ }
588
+
589
+ export function createHandler(db: DrizzleDb, rawSchema: Schema, rawOptions?: HandlerOptions): Handler {
590
+ // The naming seam: URLs, exposedTables, and relation targets historically mixed
591
+ // drizzle export keys (`texturePacks`) with SQL names (`texture_packs`), and any
592
+ // multi-word table failed one gate or the other — both spellings 404'd. Collapse
593
+ // the two domains here, once: both spellings route, and options in either domain
594
+ // apply. The root listing below stays one entry per table (the SQL name).
595
+ const namePairs = tableNamePairs(rawSchema);
596
+ const schema = expandSchema(rawSchema, namePairs);
597
+ const options = expandTableKeyedOptions(rawOptions, namePairs);
526
598
  const tableNames = new Set(Object.keys(schema));
599
+ const canonicalTables = [...new Set(Object.entries(rawSchema).map(([key, value]) => sqlNameOf(value) ?? key))];
527
600
 
528
601
  return async (request: Request): Promise<Response> => {
529
602
  const url = new URL(request.url);
@@ -583,11 +656,12 @@ export function createHandler(db: DrizzleDb, schema: Schema, options?: HandlerOp
583
656
  }
584
657
  }
585
658
 
586
- // Root path — table listing, always public
659
+ // Root path — table listing, always public. One entry per table (SQL name),
660
+ // regardless of how many spellings route to it.
587
661
  if (pathSegments.length === 0) {
588
662
  const tables = options?.exposedTables
589
- ? Array.from(tableNames).filter(t => options.exposedTables!.includes(t))
590
- : Array.from(tableNames);
663
+ ? canonicalTables.filter(t => options.exposedTables!.includes(t))
664
+ : canonicalTables;
591
665
  return json({ tables }, 200);
592
666
  }
593
667