@fayz-ai/db 0.1.1 → 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faya Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @fayz-ai/db
2
+
3
+ > The shared Drizzle spine that every Fayz plugin schema composes with.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@fayz-ai/db.svg)](https://www.npmjs.com/package/@fayz-ai/db)
6
+ [![license](https://img.shields.io/npm/l/@fayz-ai/db.svg)](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
7
+
8
+ **Status:** beta — published to npm and used across Fayz dogfood apps. Pre-1.0: minor APIs may change before 1.0.
9
+
10
+ When apps are composed from plugins, their data models have to compose too. `@fayz-ai/db` is the schema spine: a small set of canonical Drizzle tables — tenants, people, orders, appointments, products — plus the column helpers (tenant id, timestamps) that plugin schemas build on. Every plugin references the same spine, so a CRM's clients and an agenda's appointments agree on what a person and a tenant are.
11
+
12
+ It also re-exports `drizzle-orm/pg-core` so the whole stack runs on one Drizzle instance — apps compose their own tables, the spine refs, and plugin schemas without the dual-copy `PgColumn` type clashes you get from mismatched drizzle-orm versions.
13
+
14
+ ## What's inside
15
+ - **Spine tables** — `tenants`, `people`, `orders`, `appointments`, `products`, `orderItems` (Ring 0 references plugins point at, in `public`)
16
+ - **Column helpers** — `tenantId`, `timestamps`, `createdAt`
17
+ - **Re-exported pg-core** — the full `drizzle-orm/pg-core` builder surface, so every package shares one drizzle-orm instance
18
+
19
+ ## Install
20
+ ```bash
21
+ npm install @fayz-ai/db
22
+ ```
23
+ Depends on `drizzle-orm`. Import pg-core builders from here, not from `drizzle-orm` directly.
24
+
25
+ ## Usage
26
+ ```ts
27
+ import { pgTable, text, tenantId, timestamps, people } from '@fayz-ai/db'
28
+
29
+ export const notes = pgTable('notes', {
30
+ id: text('id').primaryKey(),
31
+ personId: text('person_id').references(() => people.id),
32
+ body: text('body'),
33
+ tenantId: tenantId(),
34
+ ...timestamps,
35
+ })
36
+ ```
37
+
38
+ ## Part of the Fayz SDK
39
+ The data spine beneath every plugin schema; apps compose it in their own `drizzle.config`.
40
+
41
+ ## Roadmap & contributing
42
+ Built and evolving in the open. See the [Fayz SDK roadmap](../../docs/ROADMAP.md#db) for current gaps, missing features, and good first issues.
package/dist/helpers.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES
3
- * saas_core.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this
3
+ * public.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this
4
4
  * so tenancy is identical everywhere (and RLS can assume the column exists).
5
5
  */
6
6
  export declare const tenantId: () => import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgUUIDBuilderInitial<"tenant_id">>;
package/dist/index.cjs CHANGED
@@ -3,23 +3,22 @@
3
3
  var pgCore = require('drizzle-orm/pg-core');
4
4
 
5
5
  // src/index.ts
6
- var saasCore = pgCore.pgSchema("saas_core");
7
- var tenants = saasCore.table("tenants", {
6
+ var tenants = pgCore.pgTable("tenants", {
8
7
  id: pgCore.uuid("id").primaryKey()
9
8
  });
10
- var persons = saasCore.table("persons", {
9
+ var people = pgCore.pgTable("people", {
11
10
  id: pgCore.uuid("id").primaryKey()
12
11
  });
13
- var orders = saasCore.table("orders", {
12
+ var orders = pgCore.pgTable("orders", {
14
13
  id: pgCore.uuid("id").primaryKey()
15
14
  });
16
- var bookings = saasCore.table("bookings", {
15
+ var appointments = pgCore.pgTable("appointments", {
17
16
  id: pgCore.uuid("id").primaryKey()
18
17
  });
19
- var products = saasCore.table("products", {
18
+ var products = pgCore.pgTable("products", {
20
19
  id: pgCore.uuid("id").primaryKey()
21
20
  });
22
- var orderItems = saasCore.table("order_items", {
21
+ var orderItems = pgCore.pgTable("order_items", {
23
22
  id: pgCore.uuid("id").primaryKey()
24
23
  });
25
24
  var tenantId = () => pgCore.uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" });
@@ -31,13 +30,12 @@ var createdAt = {
31
30
  createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
32
31
  };
33
32
 
34
- exports.bookings = bookings;
33
+ exports.appointments = appointments;
35
34
  exports.createdAt = createdAt;
36
35
  exports.orderItems = orderItems;
37
36
  exports.orders = orders;
38
- exports.persons = persons;
37
+ exports.people = people;
39
38
  exports.products = products;
40
- exports.saasCore = saasCore;
41
39
  exports.tenantId = tenantId;
42
40
  exports.tenants = tenants;
43
41
  exports.timestamps = timestamps;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema/spine.ts","../src/helpers.ts"],"names":["pgSchema","uuid","timestamp"],"mappings":";;;;;AAWO,IAAM,QAAA,GAAWA,gBAAS,WAAW;AAErC,IAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,SAAA,EAAW;AAAA,EAC/C,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,SAAA,EAAW;AAAA,EAC/C,EAAA,EAAIA,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAAS,QAAA,CAAS,KAAA,CAAM,QAAA,EAAU;AAAA,EAC7C,EAAA,EAAIA,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAW,QAAA,CAAS,KAAA,CAAM,UAAA,EAAY;AAAA,EACjD,EAAA,EAAIA,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAW,QAAA,CAAS,KAAA,CAAM,UAAA,EAAY;AAAA,EACjD,EAAA,EAAIA,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,aAAA,EAAe;AAAA,EACtD,EAAA,EAAIA,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AC3BM,IAAM,QAAA,GAAW,MACtBA,WAAAA,CAAK,WAAW,EACb,OAAA,EAAQ,CACR,UAAA,CAAW,MAAM,OAAA,CAAQ,EAAA,EAAI,EAAE,QAAA,EAAU,WAAW;AAGlD,IAAM,UAAA,GAAa;AAAA,EACxB,SAAA,EAAWC,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA,EAAW;AAAA,EAChF,SAAA,EAAWA,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE;AAGO,IAAM,SAAA,GAAY;AAAA,EACvB,SAAA,EAAWA,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE","file":"index.cjs","sourcesContent":["import { pgSchema, uuid } from 'drizzle-orm/pg-core'\n\n/**\n * Ring 0 — the saas_core spine, declared as Drizzle *references* only.\n *\n * These tables are owned by the platform (@fayz/saas-core) and already exist in\n * every provisioned project. We declare a minimal shape here purely so plugin\n * tables can express real cross-schema foreign keys in TypeScript. They land in\n * the Drizzle *baseline* snapshot (never re-created), so only the `id` FK target\n * is needed the live columns are authoritative.\n */\nexport const saasCore = pgSchema('saas_core')\n\nexport const tenants = saasCore.table('tenants', {\n id: uuid('id').primaryKey(),\n})\n\nexport const persons = saasCore.table('persons', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orders = saasCore.table('orders', {\n id: uuid('id').primaryKey(),\n})\n\nexport const bookings = saasCore.table('bookings', {\n id: uuid('id').primaryKey(),\n})\n\nexport const products = saasCore.table('products', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orderItems = saasCore.table('order_items', {\n id: uuid('id').primaryKey(),\n})\n","import { uuid, timestamp } from 'drizzle-orm/pg-core'\nimport { tenants } from './schema/spine'\n\n/**\n * Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES\n * saas_core.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this\n * so tenancy is identical everywhere (and RLS can assume the column exists).\n */\nexport const tenantId = () =>\n uuid('tenant_id')\n .notNull()\n .references(() => tenants.id, { onDelete: 'cascade' })\n\n/** Standard `created_at` / `updated_at` timestamptz pair with `now()` defaults. */\nexport const timestamps = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}\n\n/** Just `created_at` (for append-only / event-style tables). */\nexport const createdAt = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}\n"]}
1
+ {"version":3,"sources":["../src/schema/spine.ts","../src/helpers.ts"],"names":["pgTable","uuid","timestamp"],"mappings":";;;;;AAYO,IAAM,OAAA,GAAUA,eAAQ,SAAA,EAAW;AAAA,EACxC,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAASD,eAAQ,QAAA,EAAU;AAAA,EACtC,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAASD,eAAQ,QAAA,EAAU;AAAA,EACtC,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,YAAA,GAAeD,eAAQ,cAAA,EAAgB;AAAA,EAClD,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAWD,eAAQ,UAAA,EAAY;AAAA,EAC1C,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,UAAA,GAAaD,eAAQ,aAAA,EAAe;AAAA,EAC/C,EAAA,EAAIC,WAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AC1BM,IAAM,QAAA,GAAW,MACtBA,WAAAA,CAAK,WAAW,EACb,OAAA,EAAQ,CACR,UAAA,CAAW,MAAM,OAAA,CAAQ,EAAA,EAAI,EAAE,QAAA,EAAU,WAAW;AAGlD,IAAM,UAAA,GAAa;AAAA,EACxB,SAAA,EAAWC,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA,EAAW;AAAA,EAChF,SAAA,EAAWA,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE;AAGO,IAAM,SAAA,GAAY;AAAA,EACvB,SAAA,EAAWA,gBAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE","file":"index.cjs","sourcesContent":["import { pgTable, uuid } from 'drizzle-orm/pg-core'\n\n/**\n * Ring 0 — the core spine, declared as Drizzle *references* only.\n *\n * These tables are owned by the platform (@fayz-ai/saas core) and already exist\n * in every provisioned pool, directly in the `public` schema (industry-pool\n * model — no saas_core schema). We declare a minimal shape here purely so plugin\n * tables can express real foreign keys in TypeScript. They land in the Drizzle\n * *baseline* snapshot (never re-created), so only the `id` FK target is needed —\n * the live columns are authoritative.\n */\nexport const tenants = pgTable('tenants', {\n id: uuid('id').primaryKey(),\n})\n\nexport const people = pgTable('people', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orders = pgTable('orders', {\n id: uuid('id').primaryKey(),\n})\n\nexport const appointments = pgTable('appointments', {\n id: uuid('id').primaryKey(),\n})\n\nexport const products = pgTable('products', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orderItems = pgTable('order_items', {\n id: uuid('id').primaryKey(),\n})\n","import { uuid, timestamp } from 'drizzle-orm/pg-core'\nimport { tenants } from './schema/spine'\n\n/**\n * Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES\n * public.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this\n * so tenancy is identical everywhere (and RLS can assume the column exists).\n */\nexport const tenantId = () =>\n uuid('tenant_id')\n .notNull()\n .references(() => tenants.id, { onDelete: 'cascade' })\n\n/** Standard `created_at` / `updated_at` timestamptz pair with `now()` defaults. */\nexport const timestamps = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}\n\n/** Just `created_at` (for append-only / event-style tables). */\nexport const createdAt = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from 'drizzle-orm/pg-core';
2
- export { saasCore, tenants, persons, orders, bookings, products, orderItems } from './schema/spine';
2
+ export { tenants, people, orders, appointments, products, orderItems } from './schema/spine';
3
3
  export { tenantId, timestamps, createdAt } from './helpers';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,qBAAqB,CAAA;AAEnC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACnG,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,qBAAqB,CAAA;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC5F,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -1,24 +1,23 @@
1
- import { pgSchema, uuid, timestamp } from 'drizzle-orm/pg-core';
1
+ import { pgTable, uuid, timestamp } from 'drizzle-orm/pg-core';
2
2
  export * from 'drizzle-orm/pg-core';
3
3
 
4
4
  // src/index.ts
5
- var saasCore = pgSchema("saas_core");
6
- var tenants = saasCore.table("tenants", {
5
+ var tenants = pgTable("tenants", {
7
6
  id: uuid("id").primaryKey()
8
7
  });
9
- var persons = saasCore.table("persons", {
8
+ var people = pgTable("people", {
10
9
  id: uuid("id").primaryKey()
11
10
  });
12
- var orders = saasCore.table("orders", {
11
+ var orders = pgTable("orders", {
13
12
  id: uuid("id").primaryKey()
14
13
  });
15
- var bookings = saasCore.table("bookings", {
14
+ var appointments = pgTable("appointments", {
16
15
  id: uuid("id").primaryKey()
17
16
  });
18
- var products = saasCore.table("products", {
17
+ var products = pgTable("products", {
19
18
  id: uuid("id").primaryKey()
20
19
  });
21
- var orderItems = saasCore.table("order_items", {
20
+ var orderItems = pgTable("order_items", {
22
21
  id: uuid("id").primaryKey()
23
22
  });
24
23
  var tenantId = () => uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" });
@@ -30,6 +29,6 @@ var createdAt = {
30
29
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
31
30
  };
32
31
 
33
- export { bookings, createdAt, orderItems, orders, persons, products, saasCore, tenantId, tenants, timestamps };
32
+ export { appointments, createdAt, orderItems, orders, people, products, tenantId, tenants, timestamps };
34
33
  //# sourceMappingURL=index.js.map
35
34
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema/spine.ts","../src/helpers.ts"],"names":["uuid"],"mappings":";;;;AAWO,IAAM,QAAA,GAAW,SAAS,WAAW;AAErC,IAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,SAAA,EAAW;AAAA,EAC/C,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,SAAA,EAAW;AAAA,EAC/C,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAAS,QAAA,CAAS,KAAA,CAAM,QAAA,EAAU;AAAA,EAC7C,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAW,QAAA,CAAS,KAAA,CAAM,UAAA,EAAY;AAAA,EACjD,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAW,QAAA,CAAS,KAAA,CAAM,UAAA,EAAY;AAAA,EACjD,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,aAAA,EAAe;AAAA,EACtD,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AC3BM,IAAM,QAAA,GAAW,MACtBA,IAAAA,CAAK,WAAW,EACb,OAAA,EAAQ,CACR,UAAA,CAAW,MAAM,OAAA,CAAQ,EAAA,EAAI,EAAE,QAAA,EAAU,WAAW;AAGlD,IAAM,UAAA,GAAa;AAAA,EACxB,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA,EAAW;AAAA,EAChF,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE;AAGO,IAAM,SAAA,GAAY;AAAA,EACvB,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE","file":"index.js","sourcesContent":["import { pgSchema, uuid } from 'drizzle-orm/pg-core'\n\n/**\n * Ring 0 — the saas_core spine, declared as Drizzle *references* only.\n *\n * These tables are owned by the platform (@fayz/saas-core) and already exist in\n * every provisioned project. We declare a minimal shape here purely so plugin\n * tables can express real cross-schema foreign keys in TypeScript. They land in\n * the Drizzle *baseline* snapshot (never re-created), so only the `id` FK target\n * is needed the live columns are authoritative.\n */\nexport const saasCore = pgSchema('saas_core')\n\nexport const tenants = saasCore.table('tenants', {\n id: uuid('id').primaryKey(),\n})\n\nexport const persons = saasCore.table('persons', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orders = saasCore.table('orders', {\n id: uuid('id').primaryKey(),\n})\n\nexport const bookings = saasCore.table('bookings', {\n id: uuid('id').primaryKey(),\n})\n\nexport const products = saasCore.table('products', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orderItems = saasCore.table('order_items', {\n id: uuid('id').primaryKey(),\n})\n","import { uuid, timestamp } from 'drizzle-orm/pg-core'\nimport { tenants } from './schema/spine'\n\n/**\n * Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES\n * saas_core.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this\n * so tenancy is identical everywhere (and RLS can assume the column exists).\n */\nexport const tenantId = () =>\n uuid('tenant_id')\n .notNull()\n .references(() => tenants.id, { onDelete: 'cascade' })\n\n/** Standard `created_at` / `updated_at` timestamptz pair with `now()` defaults. */\nexport const timestamps = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}\n\n/** Just `created_at` (for append-only / event-style tables). */\nexport const createdAt = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}\n"]}
1
+ {"version":3,"sources":["../src/schema/spine.ts","../src/helpers.ts"],"names":["uuid"],"mappings":";;;;AAYO,IAAM,OAAA,GAAU,QAAQ,SAAA,EAAW;AAAA,EACxC,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAAS,QAAQ,QAAA,EAAU;AAAA,EACtC,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,MAAA,GAAS,QAAQ,QAAA,EAAU;AAAA,EACtC,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,YAAA,GAAe,QAAQ,cAAA,EAAgB;AAAA,EAClD,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,QAAA,GAAW,QAAQ,UAAA,EAAY;AAAA,EAC1C,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AAEM,IAAM,UAAA,GAAa,QAAQ,aAAA,EAAe;AAAA,EAC/C,EAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,UAAA;AACjB,CAAC;AC1BM,IAAM,QAAA,GAAW,MACtBA,IAAAA,CAAK,WAAW,EACb,OAAA,EAAQ,CACR,UAAA,CAAW,MAAM,OAAA,CAAQ,EAAA,EAAI,EAAE,QAAA,EAAU,WAAW;AAGlD,IAAM,UAAA,GAAa;AAAA,EACxB,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA,EAAW;AAAA,EAChF,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE;AAGO,IAAM,SAAA,GAAY;AAAA,EACvB,SAAA,EAAW,SAAA,CAAU,YAAA,EAAc,EAAE,YAAA,EAAc,MAAM,CAAA,CAAE,OAAA,EAAQ,CAAE,UAAA;AACvE","file":"index.js","sourcesContent":["import { pgTable, uuid } from 'drizzle-orm/pg-core'\n\n/**\n * Ring 0 — the core spine, declared as Drizzle *references* only.\n *\n * These tables are owned by the platform (@fayz-ai/saas core) and already exist\n * in every provisioned pool, directly in the `public` schema (industry-pool\n * model — no saas_core schema). We declare a minimal shape here purely so plugin\n * tables can express real foreign keys in TypeScript. They land in the Drizzle\n * *baseline* snapshot (never re-created), so only the `id` FK target is needed —\n * the live columns are authoritative.\n */\nexport const tenants = pgTable('tenants', {\n id: uuid('id').primaryKey(),\n})\n\nexport const people = pgTable('people', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orders = pgTable('orders', {\n id: uuid('id').primaryKey(),\n})\n\nexport const appointments = pgTable('appointments', {\n id: uuid('id').primaryKey(),\n})\n\nexport const products = pgTable('products', {\n id: uuid('id').primaryKey(),\n})\n\nexport const orderItems = pgTable('order_items', {\n id: uuid('id').primaryKey(),\n})\n","import { uuid, timestamp } from 'drizzle-orm/pg-core'\nimport { tenants } from './schema/spine'\n\n/**\n * Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES\n * public.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this\n * so tenancy is identical everywhere (and RLS can assume the column exists).\n */\nexport const tenantId = () =>\n uuid('tenant_id')\n .notNull()\n .references(() => tenants.id, { onDelete: 'cascade' })\n\n/** Standard `created_at` / `updated_at` timestamptz pair with `now()` defaults. */\nexport const timestamps = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n}\n\n/** Just `created_at` (for append-only / event-style tables). */\nexport const createdAt = {\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n}\n"]}
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Ring 0 — the saas_core spine, declared as Drizzle *references* only.
2
+ * Ring 0 — the core spine, declared as Drizzle *references* only.
3
3
  *
4
- * These tables are owned by the platform (@fayz/saas-core) and already exist in
5
- * every provisioned project. We declare a minimal shape here purely so plugin
6
- * tables can express real cross-schema foreign keys in TypeScript. They land in
7
- * the Drizzle *baseline* snapshot (never re-created), so only the `id` FK target
8
- * is needed the live columns are authoritative.
4
+ * These tables are owned by the platform (@fayz-ai/saas core) and already exist
5
+ * in every provisioned pool, directly in the `public` schema (industry-pool
6
+ * model no saas_core schema). We declare a minimal shape here purely so plugin
7
+ * tables can express real foreign keys in TypeScript. They land in the Drizzle
8
+ * *baseline* snapshot (never re-created), so only the `id` FK target is needed —
9
+ * the live columns are authoritative.
9
10
  */
10
- export declare const saasCore: import("drizzle-orm/pg-core").PgSchema<"saas_core">;
11
11
  export declare const tenants: import("drizzle-orm/pg-core").PgTableWithColumns<{
12
12
  name: "tenants";
13
- schema: "saas_core";
13
+ schema: undefined;
14
14
  columns: {
15
15
  id: import("drizzle-orm/pg-core").PgColumn<{
16
16
  name: "id";
@@ -32,13 +32,13 @@ export declare const tenants: import("drizzle-orm/pg-core").PgTableWithColumns<{
32
32
  };
33
33
  dialect: "pg";
34
34
  }>;
35
- export declare const persons: import("drizzle-orm/pg-core").PgTableWithColumns<{
36
- name: "persons";
37
- schema: "saas_core";
35
+ export declare const people: import("drizzle-orm/pg-core").PgTableWithColumns<{
36
+ name: "people";
37
+ schema: undefined;
38
38
  columns: {
39
39
  id: import("drizzle-orm/pg-core").PgColumn<{
40
40
  name: "id";
41
- tableName: "persons";
41
+ tableName: "people";
42
42
  dataType: "string";
43
43
  columnType: "PgUUID";
44
44
  data: string;
@@ -58,7 +58,7 @@ export declare const persons: import("drizzle-orm/pg-core").PgTableWithColumns<{
58
58
  }>;
59
59
  export declare const orders: import("drizzle-orm/pg-core").PgTableWithColumns<{
60
60
  name: "orders";
61
- schema: "saas_core";
61
+ schema: undefined;
62
62
  columns: {
63
63
  id: import("drizzle-orm/pg-core").PgColumn<{
64
64
  name: "id";
@@ -80,13 +80,13 @@ export declare const orders: import("drizzle-orm/pg-core").PgTableWithColumns<{
80
80
  };
81
81
  dialect: "pg";
82
82
  }>;
83
- export declare const bookings: import("drizzle-orm/pg-core").PgTableWithColumns<{
84
- name: "bookings";
85
- schema: "saas_core";
83
+ export declare const appointments: import("drizzle-orm/pg-core").PgTableWithColumns<{
84
+ name: "appointments";
85
+ schema: undefined;
86
86
  columns: {
87
87
  id: import("drizzle-orm/pg-core").PgColumn<{
88
88
  name: "id";
89
- tableName: "bookings";
89
+ tableName: "appointments";
90
90
  dataType: "string";
91
91
  columnType: "PgUUID";
92
92
  data: string;
@@ -106,7 +106,7 @@ export declare const bookings: import("drizzle-orm/pg-core").PgTableWithColumns<
106
106
  }>;
107
107
  export declare const products: import("drizzle-orm/pg-core").PgTableWithColumns<{
108
108
  name: "products";
109
- schema: "saas_core";
109
+ schema: undefined;
110
110
  columns: {
111
111
  id: import("drizzle-orm/pg-core").PgColumn<{
112
112
  name: "id";
@@ -130,7 +130,7 @@ export declare const products: import("drizzle-orm/pg-core").PgTableWithColumns<
130
130
  }>;
131
131
  export declare const orderItems: import("drizzle-orm/pg-core").PgTableWithColumns<{
132
132
  name: "order_items";
133
- schema: "saas_core";
133
+ schema: undefined;
134
134
  columns: {
135
135
  id: import("drizzle-orm/pg-core").PgColumn<{
136
136
  name: "id";
@@ -1 +1 @@
1
- {"version":3,"file":"spine.d.ts","sourceRoot":"","sources":["../../src/schema/spine.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,qDAAwB,CAAA;AAE7C,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;EAElB,CAAA;AAEF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;EAElB,CAAA;AAEF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;EAEjB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;EAEnB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;EAEnB,CAAA;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;EAErB,CAAA"}
1
+ {"version":3,"file":"spine.d.ts","sourceRoot":"","sources":["../../src/schema/spine.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;EAElB,CAAA;AAEF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;EAEjB,CAAA;AAEF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;EAEjB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;EAEvB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;EAEnB,CAAA;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;EAErB,CAAA"}
@@ -0,0 +1,86 @@
1
+ -- ============================================================================
2
+ -- 0000_legacy_quarantine.sql — pre-convert collision guard (ALL pools)
3
+ --
4
+ -- MUST run BEFORE 000_core_v1_convert.sql. The filename starts `0000_` so it
5
+ -- lexically sorts ahead of `000_core_v1_convert.sql` (the runner orders files
6
+ -- with a plain string sort — verify: ['0000_legacy_quarantine.sql',
7
+ -- '000_core_v1_convert.sql'].sort()).
8
+ --
9
+ -- WHY: the in-place converter moves saas_core.<source> -> public.<target> only
10
+ -- when public.<target> is FREE (guarded on target absence). If a pool already
11
+ -- holds a legacy public.<target> (a bespoke pre-pool table sharing the name),
12
+ -- the move/rename silently no-ops and the live saas_core.<source> is left
13
+ -- behind — then `DROP SCHEMA saas_core CASCADE` would DESTROY it. This file
14
+ -- gets the colliding legacy table out of the way first, into a quarantine
15
+ -- schema, so every source table has a free target to land on.
16
+ --
17
+ -- For EVERY (source -> target) pair the converter touches, if BOTH
18
+ -- saas_core.<source> AND public.<target> exist, move public.<target> into
19
+ -- legacy_pre_pools. The presence of saas_core.<source> proves the convert has
20
+ -- not run yet, so this public.<target> must be a legacy collision (not the
21
+ -- converted core table).
22
+ --
23
+ -- Known real collisions from the 2026-07-14 backups (all covered by the loop):
24
+ -- * creators — public.subscriptions (4 rows, direct-move pair)
25
+ -- * salon/gphx — public.appointments (0 rows, bookings->appointments rename)
26
+ -- * restaurant — public.orders + public.order_items (0 rows, direct-move pairs)
27
+ --
28
+ -- Idempotent: guarded on saas_core existing; each move guarded on both source
29
+ -- present AND target present; a no-op on fresh pools and on already-converted
30
+ -- pools (saas_core dropped -> early return).
31
+ -- ============================================================================
32
+
33
+ DO $quarantine$
34
+ DECLARE
35
+ r record;
36
+ BEGIN
37
+ -- Only act on pools that still carry the legacy schema (pre-convert).
38
+ IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'saas_core') THEN
39
+ RETURN;
40
+ END IF;
41
+
42
+ -- (source in saas_core) -> (target in public) for every table the converter
43
+ -- moves. Direct moves keep the name; the three changed entities are renamed
44
+ -- (persons->people, bookings->appointments, booking_items->appointment_items).
45
+ -- Mirrors the move list in 000_core_v1_convert.sql exactly.
46
+ FOR r IN
47
+ SELECT * FROM (VALUES
48
+ ('tenants', 'tenants'),
49
+ ('profiles', 'profiles'),
50
+ ('tenant_members', 'tenant_members'),
51
+ ('plans', 'plans'),
52
+ ('permissions', 'permissions'),
53
+ ('role_permissions', 'role_permissions'),
54
+ ('tenant_role_overrides', 'tenant_role_overrides'),
55
+ ('invitations', 'invitations'),
56
+ ('payment_events', 'payment_events'),
57
+ ('locations', 'locations'),
58
+ ('location_members', 'location_members'),
59
+ ('audit_logs', 'audit_logs'),
60
+ ('subscriptions', 'subscriptions'),
61
+ ('invoices', 'invoices'),
62
+ ('notifications', 'notifications'),
63
+ ('persons', 'people'),
64
+ ('categories', 'categories'),
65
+ ('products', 'products'),
66
+ ('services', 'services'),
67
+ ('orders', 'orders'),
68
+ ('order_items', 'order_items'),
69
+ ('transactions', 'transactions'),
70
+ ('bookings', 'appointments'),
71
+ ('booking_items', 'appointment_items'),
72
+ ('schedules', 'schedules'),
73
+ ('sequences', 'sequences'),
74
+ ('documents', 'documents')
75
+ ) AS m(source, target)
76
+ LOOP
77
+ IF to_regclass('saas_core.' || quote_ident(r.source)) IS NOT NULL
78
+ AND to_regclass('public.' || quote_ident(r.target)) IS NOT NULL THEN
79
+ CREATE SCHEMA IF NOT EXISTS legacy_pre_pools;
80
+ EXECUTE format('ALTER TABLE public.%I SET SCHEMA legacy_pre_pools', r.target);
81
+ RAISE NOTICE 'legacy_pre_pools: quarantined public.% (collides with the saas_core.% -> public.% move)',
82
+ r.target, r.source, r.target;
83
+ END IF;
84
+ END LOOP;
85
+ END;
86
+ $quarantine$;
@@ -0,0 +1,37 @@
1
+ -- Move any saas_core table the converter does not know about into public,
2
+ -- so the converter's emptiness gate can pass and drop the schema safely.
3
+ -- Found live: salon carries saas_core.tenant_roles (RBAC iteration that only
4
+ -- ever existed there). Runs between 0000_legacy_quarantine and
5
+ -- 000_core_v1_convert ('0000b' < '000_' lexically). Idempotent: no-ops when
6
+ -- saas_core is gone or has no stragglers. A public-name collision aborts —
7
+ -- that case must be quarantined explicitly, never guessed.
8
+
9
+ DO $sweep$
10
+ DECLARE
11
+ r record;
12
+ known text[] := ARRAY[
13
+ 'tenants', 'profiles', 'tenant_members', 'plans', 'permissions',
14
+ 'role_permissions', 'tenant_role_overrides', 'invitations',
15
+ 'payment_events', 'locations', 'location_members', 'audit_logs',
16
+ 'subscriptions', 'invoices', 'notifications',
17
+ 'persons', 'categories', 'products', 'services', 'orders', 'order_items',
18
+ 'transactions', 'bookings', 'booking_items', 'schedules',
19
+ 'sequences', 'documents'
20
+ ];
21
+ BEGIN
22
+ IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'saas_core') THEN
23
+ RETURN;
24
+ END IF;
25
+
26
+ FOR r IN
27
+ SELECT table_name FROM information_schema.tables
28
+ WHERE table_schema = 'saas_core' AND table_type = 'BASE TABLE'
29
+ AND table_name <> ALL (known)
30
+ LOOP
31
+ IF to_regclass('public.' || quote_ident(r.table_name)) IS NOT NULL THEN
32
+ RAISE EXCEPTION 'straggler saas_core.% collides with an existing public table — quarantine it explicitly', r.table_name;
33
+ END IF;
34
+ EXECUTE format('ALTER TABLE saas_core.%I SET SCHEMA public', r.table_name);
35
+ RAISE NOTICE 'straggler moved: saas_core.% -> public', r.table_name;
36
+ END LOOP;
37
+ END $sweep$;