@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 +21 -0
- package/README.md +42 -0
- package/dist/helpers.d.ts +1 -1
- package/dist/index.cjs +8 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +19 -19
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/0000_legacy_quarantine.sql +86 -0
- package/migrations/0000b_straggler_sweep.sql +37 -0
- package/migrations/000_core_v1_convert.sql +343 -0
- package/migrations/001_core.sql +372 -0
- package/migrations/002_rls_user_tenant_ids.sql +63 -0
- package/migrations/003_invitations.sql +17 -0
- package/migrations/004_archetypes.sql +343 -0
- package/migrations/005_locations_archetype.sql +18 -0
- package/migrations/006_archetype_rls.sql +74 -0
- package/migrations/007_archetype_grants.sql +4 -0
- package/migrations/008_grants.sql +5 -0
- package/migrations/009_anon_hardening.sql +19 -0
- package/migrations/010_migration_ledger.sql +30 -0
- package/migrations/011_anon_write_revoke.sql +12 -0
- package/package.json +6 -5
- package/src/helpers.ts +1 -1
- package/src/index.ts +1 -1
- package/src/schema/spine.ts +14 -15
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
|
+
[](https://www.npmjs.com/package/@fayz-ai/db)
|
|
6
|
+
[](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
|
-
*
|
|
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
|
|
7
|
-
var tenants = saasCore.table("tenants", {
|
|
6
|
+
var tenants = pgCore.pgTable("tenants", {
|
|
8
7
|
id: pgCore.uuid("id").primaryKey()
|
|
9
8
|
});
|
|
10
|
-
var
|
|
9
|
+
var people = pgCore.pgTable("people", {
|
|
11
10
|
id: pgCore.uuid("id").primaryKey()
|
|
12
11
|
});
|
|
13
|
-
var orders =
|
|
12
|
+
var orders = pgCore.pgTable("orders", {
|
|
14
13
|
id: pgCore.uuid("id").primaryKey()
|
|
15
14
|
});
|
|
16
|
-
var
|
|
15
|
+
var appointments = pgCore.pgTable("appointments", {
|
|
17
16
|
id: pgCore.uuid("id").primaryKey()
|
|
18
17
|
});
|
|
19
|
-
var products =
|
|
18
|
+
var products = pgCore.pgTable("products", {
|
|
20
19
|
id: pgCore.uuid("id").primaryKey()
|
|
21
20
|
});
|
|
22
|
-
var orderItems =
|
|
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.
|
|
33
|
+
exports.appointments = appointments;
|
|
35
34
|
exports.createdAt = createdAt;
|
|
36
35
|
exports.orderItems = orderItems;
|
|
37
36
|
exports.orders = orders;
|
|
38
|
-
exports.
|
|
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;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema/spine.ts","../src/helpers.ts"],"names":["
|
|
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 {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,qBAAqB,CAAA;AAEnC,OAAO,EAAE,
|
|
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 {
|
|
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
|
|
6
|
-
var tenants = saasCore.table("tenants", {
|
|
5
|
+
var tenants = pgTable("tenants", {
|
|
7
6
|
id: uuid("id").primaryKey()
|
|
8
7
|
});
|
|
9
|
-
var
|
|
8
|
+
var people = pgTable("people", {
|
|
10
9
|
id: uuid("id").primaryKey()
|
|
11
10
|
});
|
|
12
|
-
var orders =
|
|
11
|
+
var orders = pgTable("orders", {
|
|
13
12
|
id: uuid("id").primaryKey()
|
|
14
13
|
});
|
|
15
|
-
var
|
|
14
|
+
var appointments = pgTable("appointments", {
|
|
16
15
|
id: uuid("id").primaryKey()
|
|
17
16
|
});
|
|
18
|
-
var products =
|
|
17
|
+
var products = pgTable("products", {
|
|
19
18
|
id: uuid("id").primaryKey()
|
|
20
19
|
});
|
|
21
|
-
var orderItems =
|
|
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 {
|
|
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":";;;;
|
|
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"]}
|
package/dist/schema/spine.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ring 0 — the
|
|
2
|
+
* Ring 0 — the core spine, declared as Drizzle *references* only.
|
|
3
3
|
*
|
|
4
|
-
* These tables are owned by the platform (@fayz/saas
|
|
5
|
-
* every provisioned
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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:
|
|
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
|
|
36
|
-
name: "
|
|
37
|
-
schema:
|
|
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: "
|
|
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:
|
|
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
|
|
84
|
-
name: "
|
|
85
|
-
schema:
|
|
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: "
|
|
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:
|
|
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:
|
|
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
|
|
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$;
|