@12-apps/prisma 1.0.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 +95 -0
- package/dist/actor-context.d.ts +127 -0
- package/dist/actor-context.js +139 -0
- package/dist/append-only-extension.d.ts +28 -0
- package/dist/append-only-extension.js +72 -0
- package/dist/audit-extension.d.ts +18 -0
- package/dist/audit-extension.js +123 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +213 -0
- package/dist/search-normalize.d.ts +13 -0
- package/dist/search-normalize.js +20 -0
- package/package.json +96 -0
- package/prisma/migration-files.ts +33 -0
- package/prisma/migrations/20260725150000_payments_platform_core/migration.sql +98 -0
- package/prisma/migrations/20260725160000_payments_oauth_connections/migration.sql +25 -0
- package/prisma/migrations/20260725170000_add_saved_reports/migration.sql +22 -0
- package/prisma/migrations/20260726090000_payments_multi_provider_failover/migration.sql +67 -0
- package/prisma/migrations/20260726120000_payments_failover_policy/migration.sql +29 -0
- package/prisma/migrations/20260726130000_add_report_lifecycle/migration.sql +18 -0
- package/prisma/migrations/20260727120000_add_report_archived_status/migration.sql +9 -0
- package/prisma/migrations/20260727120000_payments_webhook_replay_budget/migration.sql +37 -0
- package/prisma/migrations/20260727190000_sweep_leases/migration.sql +13 -0
- package/prisma/migrations/20260728120000_add_product_research/migration.sql +112 -0
- package/prisma/migrations/20260728170000_add_manual_price_entries/migration.sql +31 -0
- package/prisma/migrations/20260729090000_integration_source_singleton/migration.sql +24 -0
- package/prisma/migrations/20260729120000_price_source_soft_delete/migration.sql +24 -0
- package/prisma/migrations/20260729140000_research_term_normalized/migration.sql +26 -0
- package/prisma/migrations/20260730120000_offer_outside_delivery_area/migration.sql +16 -0
- package/prisma/migrations/20260730120000_payments_charge_verified_at/migration.sql +23 -0
- package/prisma/migrations/20260730130000_research_runs_created_at_index/migration.sql +12 -0
- package/prisma/migrations/20260730210000_add_shifts/migration.sql +95 -0
- package/prisma/migrations/20260731000000_offer_shipping_unknown/migration.sql +34 -0
- package/prisma/migrations/20260731210000_shift_delete_guard/migration.sql +43 -0
- package/prisma/migrations/20260810120000_add_report_default_range/migration.sql +15 -0
- package/prisma/migrations/20260810160000_report_default_range_month/migration.sql +15 -0
- package/prisma/migrations/20260810180000_add_report_working_copy/migration.sql +13 -0
- package/prisma/plugin-migrations.json +28 -0
- package/prisma/schema/entity-lifecycle.prisma +126 -0
- package/prisma/schema/jobs.prisma +48 -0
- package/prisma/schema/product-research.prisma +217 -0
- package/prisma/schema/schema.prisma +19 -0
- package/prisma/schema/shift.prisma +34 -0
- package/src/actor-context.ts +218 -0
- package/src/append-only-extension.ts +75 -0
- package/src/audit-extension.ts +121 -0
- package/src/index.ts +233 -0
- package/src/search-normalize.ts +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Prisma client singleton.
|
|
4
|
+
*
|
|
5
|
+
* Default mode builds a PostgreSQL-backed `PrismaClient` (the real / production
|
|
6
|
+
* database, driven by `DATABASE_URL`). When a PGlite flag is present in the
|
|
7
|
+
* environment the same client is built on the community `pglite-prisma-adapter`
|
|
8
|
+
* so PGlite (WASM Postgres) becomes the app runtime database for local dev and
|
|
9
|
+
* Playwright e2e — no Docker Postgres required.
|
|
10
|
+
*
|
|
11
|
+
* PGlite mode is selected by ANY of:
|
|
12
|
+
* - `USE_FILE_DB=1` (or `true`) → in-memory, unless a dir is also set
|
|
13
|
+
* - `PGLITE_DATA_DIR=<dir>` → file-backed at <dir>
|
|
14
|
+
* - `DATABASE_URL` starting with `pglite:` → `pglite:memory` or `pglite:<dir>`
|
|
15
|
+
*
|
|
16
|
+
* Production safety: when `NODE_ENV=production` the database is ALWAYS real
|
|
17
|
+
* PostgreSQL unless `USE_FILE_DB=1` is set explicitly — so a stray
|
|
18
|
+
* `PGLITE_DATA_DIR` / `pglite:` URL can never silently point prod at a throwaway
|
|
19
|
+
* WASM DB. `USE_FILE_DB=0` forces real PostgreSQL everywhere (dev/e2e opt-out).
|
|
20
|
+
*
|
|
21
|
+
* The generated `PrismaClient` type is re-exported below so consumers stay fully
|
|
22
|
+
* typed without any hand-written stub interfaces. Server-only: never import this
|
|
23
|
+
* (directly or transitively) from middleware or an Edge-runtime route — PGlite
|
|
24
|
+
* needs Node's filesystem/WASM and Prisma is not Edge-safe.
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.AppendOnlyViolationError = void 0;
|
|
61
|
+
const append_only_extension_1 = require("./append-only-extension");
|
|
62
|
+
const audit_extension_1 = require("./audit-extension");
|
|
63
|
+
// Append-only guard for the audit log (FUT-209): mutating the AuditLog model
|
|
64
|
+
// throws. Re-exported so tests can assert on the error type.
|
|
65
|
+
var append_only_extension_2 = require("./append-only-extension");
|
|
66
|
+
Object.defineProperty(exports, "AppendOnlyViolationError", { enumerable: true, get: function () { return append_only_extension_2.AppendOnlyViolationError; } });
|
|
67
|
+
// Change-attribution context helpers (FUT-168): the auth layer calls `setActor`
|
|
68
|
+
// once a request is authorized; the audit extension applied below reads it to
|
|
69
|
+
// stamp created_by/updated_by. Re-exported here so consumers import them from
|
|
70
|
+
// the same `@12-apps/prisma` entry point as `getPrismaClient`.
|
|
71
|
+
var actor_context_1 = require("./actor-context");
|
|
72
|
+
Object.defineProperty(exports, "getActorAttribution", { enumerable: true, get: function () { return actor_context_1.getActorAttribution; } });
|
|
73
|
+
Object.defineProperty(exports, "getActorUserId", { enumerable: true, get: function () { return actor_context_1.getActorUserId; } });
|
|
74
|
+
Object.defineProperty(exports, "runWithActor", { enumerable: true, get: function () { return actor_context_1.runWithActor; } });
|
|
75
|
+
Object.defineProperty(exports, "runWithActorScope", { enumerable: true, get: function () { return actor_context_1.runWithActorScope; } });
|
|
76
|
+
Object.defineProperty(exports, "setActor", { enumerable: true, get: function () { return actor_context_1.setActor; } });
|
|
77
|
+
var search_normalize_1 = require("./search-normalize");
|
|
78
|
+
Object.defineProperty(exports, "normalizeSearchText", { enumerable: true, get: function () { return search_normalize_1.normalizeSearchText; } });
|
|
79
|
+
// The singleton and its in-flight init promise live on `globalThis` so that
|
|
80
|
+
// Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
|
|
81
|
+
// a second PGlite instance against the same dataDir — PGlite holds a single
|
|
82
|
+
// exclusive connection, and a duplicate would deadlock or corrupt the store.
|
|
83
|
+
const globalStore = globalThis;
|
|
84
|
+
/**
|
|
85
|
+
* Prisma log levels.
|
|
86
|
+
*
|
|
87
|
+
* Full `query` logging prints every SQL statement (table/column names) to the
|
|
88
|
+
* server console, so it is OFF by default — even in development — to avoid
|
|
89
|
+
* surfacing schema details and noise. Opt in locally with `PRISMA_LOG_QUERIES=1`.
|
|
90
|
+
* Production logs errors only.
|
|
91
|
+
*/
|
|
92
|
+
const prismaLog = () => {
|
|
93
|
+
if (process.env.NODE_ENV === 'production')
|
|
94
|
+
return ['error'];
|
|
95
|
+
return process.env.PRISMA_LOG_QUERIES === '1'
|
|
96
|
+
? ['query', 'error', 'warn']
|
|
97
|
+
: ['error', 'warn'];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Whether the environment selects PGlite over PostgreSQL. Production is
|
|
101
|
+
* PostgreSQL unless a file DB is DELIBERATELY forced on; an explicit
|
|
102
|
+
* `USE_FILE_DB=0` always wins.
|
|
103
|
+
*/
|
|
104
|
+
const isPgliteSelected = (env) => {
|
|
105
|
+
if (env.forcedOff)
|
|
106
|
+
return false;
|
|
107
|
+
if (process.env.NODE_ENV === 'production')
|
|
108
|
+
return env.forcedOn;
|
|
109
|
+
return env.forcedOn || env.explicitDir !== undefined || env.urlIsPglite;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the PGlite target once selected: an explicit dir wins, else a
|
|
113
|
+
* `pglite:` URL (`pglite:memory` / `pglite://memory` ⇒ in-memory; `pglite:./dir`
|
|
114
|
+
* ⇒ file), else `USE_FILE_DB` with no directory ⇒ in-memory.
|
|
115
|
+
*/
|
|
116
|
+
const pgliteTarget = (env) => {
|
|
117
|
+
if (env.explicitDir)
|
|
118
|
+
return { dataDir: env.explicitDir };
|
|
119
|
+
if (env.urlIsPglite) {
|
|
120
|
+
const raw = env.url.replace(/^pglite:(\/\/)?/, '');
|
|
121
|
+
return raw === '' || raw === 'memory' ? {} : { dataDir: raw };
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
};
|
|
125
|
+
const resolvePglite = () => {
|
|
126
|
+
const url = process.env.DATABASE_URL ?? '';
|
|
127
|
+
const flag = (process.env.USE_FILE_DB ?? '').toLowerCase();
|
|
128
|
+
const env = {
|
|
129
|
+
url,
|
|
130
|
+
explicitDir: process.env.PGLITE_DATA_DIR,
|
|
131
|
+
forcedOn: flag === '1' || flag === 'true',
|
|
132
|
+
forcedOff: flag === '0' || flag === 'false',
|
|
133
|
+
urlIsPglite: url.startsWith('pglite:'),
|
|
134
|
+
};
|
|
135
|
+
return isPgliteSelected(env) ? pgliteTarget(env) : null;
|
|
136
|
+
};
|
|
137
|
+
/** Build a PGlite-backed `PrismaClient` via the community driver adapter. */
|
|
138
|
+
const createPgliteClient = async (dataDir) => {
|
|
139
|
+
const { PGlite } = await Promise.resolve().then(() => __importStar(require('@electric-sql/pglite')));
|
|
140
|
+
// `pglite-prisma-adapter` is `exports`-only; type the dynamic import locally
|
|
141
|
+
// so it resolves under this package's classic ("Node") module resolution
|
|
142
|
+
// without a static type dependency on the package.
|
|
143
|
+
const { PrismaPGlite } = (await Promise.resolve().then(() => __importStar(require('pglite-prisma-adapter'))));
|
|
144
|
+
const { PrismaClient: GeneratedPrismaClient } = await Promise.resolve().then(() => __importStar(require('@prisma/client')));
|
|
145
|
+
const client = dataDir ? new PGlite(dataDir) : new PGlite();
|
|
146
|
+
await client.waitReady;
|
|
147
|
+
const adapter = new PrismaPGlite(client);
|
|
148
|
+
return (0, append_only_extension_1.applyAppendOnlyGuard)((0, audit_extension_1.applyAuditStamps)(new GeneratedPrismaClient({ adapter, log: prismaLog() })));
|
|
149
|
+
};
|
|
150
|
+
/** Build the default PostgreSQL-backed `PrismaClient` (real / production DB). */
|
|
151
|
+
const createPostgresClient = async () => {
|
|
152
|
+
const { PrismaPg } = await Promise.resolve().then(() => __importStar(require('@prisma/adapter-pg')));
|
|
153
|
+
const { PrismaClient: GeneratedPrismaClient } = await Promise.resolve().then(() => __importStar(require('@prisma/client')));
|
|
154
|
+
const connectionString = process.env.DATABASE_URL;
|
|
155
|
+
if (!connectionString) {
|
|
156
|
+
throw new Error('DATABASE_URL is required for the PostgreSQL Prisma client. Set it, or ' +
|
|
157
|
+
'enable PGlite mode (USE_FILE_DB=1 / PGLITE_DATA_DIR / a "pglite:" URL).');
|
|
158
|
+
}
|
|
159
|
+
// Prisma 7 makes a driver adapter mandatory — PrismaClient no longer reads
|
|
160
|
+
// DATABASE_URL itself, and `new PrismaClient()` without an adapter throws at
|
|
161
|
+
// construction. PrismaPg takes a pg.PoolConfig and owns the pool internally.
|
|
162
|
+
const adapter = new PrismaPg({
|
|
163
|
+
connectionString,
|
|
164
|
+
});
|
|
165
|
+
return (0, append_only_extension_1.applyAppendOnlyGuard)((0, audit_extension_1.applyAuditStamps)(new GeneratedPrismaClient({ adapter, log: prismaLog() })));
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Get or create the Prisma client instance.
|
|
169
|
+
*
|
|
170
|
+
* Lazy and memoised: concurrent callers share a single in-flight init promise,
|
|
171
|
+
* so only one client (and at most one PGlite instance) is ever created. A
|
|
172
|
+
* missing generated client surfaces an actionable error rather than a hard
|
|
173
|
+
* module-load failure.
|
|
174
|
+
*/
|
|
175
|
+
const getPrismaClient = async () => {
|
|
176
|
+
if (globalStore.__futurePayPrisma)
|
|
177
|
+
return globalStore.__futurePayPrisma;
|
|
178
|
+
if (globalStore.__futurePayPrismaInit)
|
|
179
|
+
return globalStore.__futurePayPrismaInit;
|
|
180
|
+
const pglite = resolvePglite();
|
|
181
|
+
const init = (pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient())
|
|
182
|
+
.then((client) => {
|
|
183
|
+
globalStore.__futurePayPrisma = client;
|
|
184
|
+
globalStore.__futurePayPrismaInit = undefined;
|
|
185
|
+
return client;
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
globalStore.__futurePayPrismaInit = undefined;
|
|
189
|
+
throw new Error(`Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
|
|
190
|
+
'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
|
|
191
|
+
'monorepo root, or "pnpm prisma generate" from packages/prisma. ' +
|
|
192
|
+
`Cause: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
193
|
+
});
|
|
194
|
+
globalStore.__futurePayPrismaInit = init;
|
|
195
|
+
return init;
|
|
196
|
+
};
|
|
197
|
+
exports.getPrismaClient = getPrismaClient;
|
|
198
|
+
/**
|
|
199
|
+
* Set a custom Prisma client instance (for testing).
|
|
200
|
+
*/
|
|
201
|
+
const setPrismaClient = (client) => {
|
|
202
|
+
globalStore.__futurePayPrisma = client;
|
|
203
|
+
globalStore.__futurePayPrismaInit = undefined;
|
|
204
|
+
};
|
|
205
|
+
exports.setPrismaClient = setPrismaClient;
|
|
206
|
+
/**
|
|
207
|
+
* Reset the Prisma client instance (for testing).
|
|
208
|
+
*/
|
|
209
|
+
const resetPrismaClient = () => {
|
|
210
|
+
globalStore.__futurePayPrisma = undefined;
|
|
211
|
+
globalStore.__futurePayPrismaInit = undefined;
|
|
212
|
+
};
|
|
213
|
+
exports.resetPrismaClient = resetPrismaClient;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accent- and case-insensitive search normalization (FUT-168). Used to keep a
|
|
3
|
+
* denormalized `search_name` column on searchable models (maintained by the
|
|
4
|
+
* audit extension) and to normalize the query at read time, so "sabao" matches
|
|
5
|
+
* "Sabão". DB-agnostic: plain lowercased, diacritic-stripped text works
|
|
6
|
+
* identically on PostgreSQL and PGlite (no `unaccent` extension needed).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Strip diacritics and lowercase. NFD splits accented letters into base +
|
|
10
|
+
* combining mark, then the marks are removed. Using the explicit code-point
|
|
11
|
+
* range (not `\p{Diacritic}`) keeps it valid under the package's compile target.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeSearchText(value: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Accent- and case-insensitive search normalization (FUT-168). Used to keep a
|
|
4
|
+
* denormalized `search_name` column on searchable models (maintained by the
|
|
5
|
+
* audit extension) and to normalize the query at read time, so "sabao" matches
|
|
6
|
+
* "Sabão". DB-agnostic: plain lowercased, diacritic-stripped text works
|
|
7
|
+
* identically on PostgreSQL and PGlite (no `unaccent` extension needed).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.normalizeSearchText = normalizeSearchText;
|
|
11
|
+
/** Unicode combining diacritical marks (U+0300–U+036F), split out by NFD. */
|
|
12
|
+
const COMBINING_MARKS = /[̀-ͯ]/g;
|
|
13
|
+
/**
|
|
14
|
+
* Strip diacritics and lowercase. NFD splits accented letters into base +
|
|
15
|
+
* combining mark, then the marks are removed. Using the explicit code-point
|
|
16
|
+
* range (not `\p{Diacritic}`) keeps it valid under the package's compile target.
|
|
17
|
+
*/
|
|
18
|
+
function normalizeSearchText(value) {
|
|
19
|
+
return value.normalize('NFD').replace(COMBINING_MARKS, '').toLowerCase();
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@12-apps/prisma",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate && tsc",
|
|
16
|
+
"clean": "rm -rf dist node_modules coverage",
|
|
17
|
+
"test": "node ../../scripts/vitest-with-teardown.mjs run",
|
|
18
|
+
"test:watch": "vitest watch",
|
|
19
|
+
"lint:files": "bash -c 'eslint \"${@:-.}\" --max-warnings 0' _",
|
|
20
|
+
"lint": "pnpm run lint:files .",
|
|
21
|
+
"lint:fix": "bash -c 'eslint \"${@:-.}\" --max-warnings 0 --fix' _",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prisma:generate": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate",
|
|
24
|
+
"prisma:migrate": "prisma migrate dev",
|
|
25
|
+
"prisma:push": "prisma db push",
|
|
26
|
+
"prisma:studio": "prisma studio",
|
|
27
|
+
"prisma:sync-lifecycle": "node scripts/sync-lifecycle-schema.mjs",
|
|
28
|
+
"prisma:sync-research": "node scripts/sync-research-schema.mjs",
|
|
29
|
+
"prisma:sync-shift": "node scripts/sync-shift-schema.mjs",
|
|
30
|
+
"prisma:sync-shift:check": "node scripts/sync-shift-schema.mjs --check",
|
|
31
|
+
"prisma:sync-jobs": "node scripts/sync-jobs-schema.mjs",
|
|
32
|
+
"prisma:sync-jobs:check": "node scripts/sync-jobs-schema.mjs --check",
|
|
33
|
+
"prisma:sync-plugins": "node scripts/sync-prisma-plugins.mjs",
|
|
34
|
+
"prisma:sync-plugins:check": "node scripts/sync-prisma-plugins.mjs --check"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@electric-sql/pglite": "0.2.17",
|
|
38
|
+
"@prisma/adapter-pg": "7.8.0",
|
|
39
|
+
"@prisma/client": "7.8.0",
|
|
40
|
+
"dotenv": "^16.4.7",
|
|
41
|
+
"pglite-prisma-adapter": "0.7.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@12-apps/entity-lifecycle": "^2.0.0",
|
|
45
|
+
"@12-apps/eslint-config": "^1.20.0",
|
|
46
|
+
"@12-apps/jobs": "^2.0.0",
|
|
47
|
+
"@12-apps/payments-backend": "^2.0.0",
|
|
48
|
+
"@12-apps/product-research": "^2.0.0",
|
|
49
|
+
"@12-apps/report-builder": "^3.0.0",
|
|
50
|
+
"@12-apps/shift": "^2.0.0",
|
|
51
|
+
"@12-apps/typescript-config": "^1.20.0",
|
|
52
|
+
"@types/node": "^22.10.6",
|
|
53
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
54
|
+
"eslint": "^9.39.1",
|
|
55
|
+
"eslint-plugin-test-flakiness": "^1.4.0",
|
|
56
|
+
"prisma": "7.8.0",
|
|
57
|
+
"typescript": "^5.8.2",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"typescript": "^5.0.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=22.0.0"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"prisma",
|
|
68
|
+
"migrations",
|
|
69
|
+
"postgres",
|
|
70
|
+
"pglite",
|
|
71
|
+
"typescript"
|
|
72
|
+
],
|
|
73
|
+
"license": "MIT",
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"registry": "https://registry.npmjs.org",
|
|
76
|
+
"access": "public"
|
|
77
|
+
},
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "git+https://github.com/12-apps/shared-packages.git",
|
|
81
|
+
"directory": "packages/prisma"
|
|
82
|
+
},
|
|
83
|
+
"files": [
|
|
84
|
+
"src",
|
|
85
|
+
"dist",
|
|
86
|
+
"prisma",
|
|
87
|
+
"*.js",
|
|
88
|
+
"*.mjs",
|
|
89
|
+
"*.md",
|
|
90
|
+
"!eslint.config.js",
|
|
91
|
+
"!**/__tests__/**",
|
|
92
|
+
"!**/tests/**",
|
|
93
|
+
"!**/*.test.*",
|
|
94
|
+
"!**/*.spec.*"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which committed migrations exist, and in what order to replay them.
|
|
7
|
+
*
|
|
8
|
+
* Its own module — with NO side effects — so both the provisioning script and
|
|
9
|
+
* its test can use it. (`pglite-setup.ts` provisions a database on import.)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/** The folder holding every committed migration, including symlinked ones. */
|
|
15
|
+
const MIGRATIONS_DIR = join(HERE, 'migrations');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every migration directory under `dir`, in timestamp order.
|
|
19
|
+
*
|
|
20
|
+
* `statSync` rather than `Dirent.isDirectory()` on purpose: a package that OWNS
|
|
21
|
+
* part of the schema contributes its migrations as committed SYMLINKS into this
|
|
22
|
+
* folder (Prisma has no cross-package import), and `Dirent.isDirectory()` is
|
|
23
|
+
* FALSE for a symlink. That silently dropped those migrations and left the
|
|
24
|
+
* package's tables missing from every PGlite-backed run — with nothing failing
|
|
25
|
+
* loudly, because the schema was merely incomplete until something queried
|
|
26
|
+
* them. `statSync` follows the link, so an owned migration replays like any
|
|
27
|
+
* other.
|
|
28
|
+
*/
|
|
29
|
+
export function discoverMigrations(dir: string = MIGRATIONS_DIR): string[] {
|
|
30
|
+
return readdirSync(dir)
|
|
31
|
+
.filter((name) => /^\d/.test(name) && statSync(join(dir, name)).isDirectory())
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: the three self-contained payment
|
|
2
|
+
-- tables (provider configs, charges, webhook inbox). No FK into host tables —
|
|
3
|
+
-- merchant scope is (merchant_kind, merchant_id). Copy this file into the
|
|
4
|
+
-- host's prisma/migrations folder (keep the timestamp ordering) after running
|
|
5
|
+
-- `prisma:sync`; CHECK constraints carry the String-over-enum closed sets.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE "payment_provider_configs" (
|
|
8
|
+
"id" TEXT NOT NULL,
|
|
9
|
+
"merchant_kind" TEXT NOT NULL,
|
|
10
|
+
"merchant_id" TEXT NOT NULL,
|
|
11
|
+
"provider" TEXT NOT NULL,
|
|
12
|
+
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
|
13
|
+
"environment" TEXT NOT NULL DEFAULT 'SANDBOX',
|
|
14
|
+
"status" TEXT NOT NULL DEFAULT 'UNVERIFIED',
|
|
15
|
+
"last_verified_at" TIMESTAMP(3),
|
|
16
|
+
"stub" BOOLEAN NOT NULL DEFAULT false,
|
|
17
|
+
"credentials" TEXT NOT NULL,
|
|
18
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
20
|
+
|
|
21
|
+
CONSTRAINT "payment_provider_configs_pkey" PRIMARY KEY ("id"),
|
|
22
|
+
CONSTRAINT "payment_provider_configs_merchant_kind_check"
|
|
23
|
+
CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
|
|
24
|
+
CONSTRAINT "payment_provider_configs_environment_check"
|
|
25
|
+
CHECK ("environment" IN ('SANDBOX', 'PRODUCTION')),
|
|
26
|
+
CONSTRAINT "payment_provider_configs_status_check"
|
|
27
|
+
CHECK ("status" IN ('UNVERIFIED', 'VERIFIED', 'FAILED'))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE UNIQUE INDEX "payment_provider_configs_merchant_kind_merchant_id_provider_key"
|
|
31
|
+
ON "payment_provider_configs"("merchant_kind", "merchant_id", "provider");
|
|
32
|
+
CREATE INDEX "payment_provider_configs_merchant_kind_merchant_id_idx"
|
|
33
|
+
ON "payment_provider_configs"("merchant_kind", "merchant_id");
|
|
34
|
+
-- SINGLE ACTIVE PROVIDER, enforced by the database rather than by app code:
|
|
35
|
+
-- at most one enabled config per merchant. A racing "enable" now fails loudly
|
|
36
|
+
-- on this constraint instead of silently leaving two providers enabled (which
|
|
37
|
+
-- would make charge routing depend on row order). Partial indexes are not
|
|
38
|
+
-- expressible in the Prisma schema — same migration-only pattern the repo
|
|
39
|
+
-- already uses for its CHECK constraints.
|
|
40
|
+
CREATE UNIQUE INDEX "payment_provider_configs_one_enabled_per_merchant"
|
|
41
|
+
ON "payment_provider_configs"("merchant_kind", "merchant_id") WHERE "enabled";
|
|
42
|
+
|
|
43
|
+
CREATE TABLE "payment_charges" (
|
|
44
|
+
"id" TEXT NOT NULL,
|
|
45
|
+
"merchant_kind" TEXT NOT NULL,
|
|
46
|
+
"merchant_id" TEXT NOT NULL,
|
|
47
|
+
"provider" TEXT NOT NULL,
|
|
48
|
+
"provider_charge_id" TEXT NOT NULL,
|
|
49
|
+
"reference" TEXT NOT NULL,
|
|
50
|
+
"idempotency_key" TEXT,
|
|
51
|
+
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
|
52
|
+
"method" TEXT NOT NULL,
|
|
53
|
+
"amount_cents" INTEGER NOT NULL,
|
|
54
|
+
"currency" TEXT NOT NULL DEFAULT 'BRL',
|
|
55
|
+
"snapshot" JSONB NOT NULL,
|
|
56
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
57
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
58
|
+
|
|
59
|
+
CONSTRAINT "payment_charges_pkey" PRIMARY KEY ("id"),
|
|
60
|
+
CONSTRAINT "payment_charges_merchant_kind_check"
|
|
61
|
+
CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
|
|
62
|
+
CONSTRAINT "payment_charges_status_check"
|
|
63
|
+
CHECK ("status" IN ('PENDING', 'AUTHORIZED', 'PAID', 'DECLINED', 'CANCELED', 'EXPIRED', 'REFUNDED', 'PARTIALLY_REFUNDED')),
|
|
64
|
+
CONSTRAINT "payment_charges_method_check"
|
|
65
|
+
CHECK ("method" IN ('PIX', 'CARD', 'BOLETO'))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE UNIQUE INDEX "payment_charges_provider_provider_charge_id_key"
|
|
69
|
+
ON "payment_charges"("provider", "provider_charge_id");
|
|
70
|
+
CREATE UNIQUE INDEX "payment_charges_merchant_kind_merchant_id_idempotency_key_key"
|
|
71
|
+
ON "payment_charges"("merchant_kind", "merchant_id", "idempotency_key");
|
|
72
|
+
CREATE INDEX "payment_charges_merchant_kind_merchant_id_created_at_idx"
|
|
73
|
+
ON "payment_charges"("merchant_kind", "merchant_id", "created_at");
|
|
74
|
+
CREATE INDEX "payment_charges_reference_idx" ON "payment_charges"("reference");
|
|
75
|
+
|
|
76
|
+
CREATE TABLE "payment_webhook_events" (
|
|
77
|
+
"id" TEXT NOT NULL,
|
|
78
|
+
"merchant_kind" TEXT NOT NULL,
|
|
79
|
+
"merchant_id" TEXT NOT NULL,
|
|
80
|
+
"provider" TEXT NOT NULL,
|
|
81
|
+
"event_id" TEXT NOT NULL,
|
|
82
|
+
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
|
83
|
+
"attempts" INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
"last_error" TEXT,
|
|
85
|
+
"headers" TEXT,
|
|
86
|
+
"payload" TEXT NOT NULL,
|
|
87
|
+
"received_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
88
|
+
"processed_at" TIMESTAMP(3),
|
|
89
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
90
|
+
|
|
91
|
+
CONSTRAINT "payment_webhook_events_pkey" PRIMARY KEY ("id"),
|
|
92
|
+
CONSTRAINT "payment_webhook_events_status_check"
|
|
93
|
+
CHECK ("status" IN ('PENDING', 'PROCESSED', 'FAILED'))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE UNIQUE INDEX "payment_webhook_events_merchant_provider_event_key"
|
|
97
|
+
ON "payment_webhook_events"("merchant_kind", "merchant_id", "provider", "event_id");
|
|
98
|
+
CREATE INDEX "payment_webhook_events_status_idx" ON "payment_webhook_events"("status");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: OAuth connection support.
|
|
2
|
+
--
|
|
3
|
+
-- `expires_at` is a first-class column rather than another key inside the
|
|
4
|
+
-- encrypted `credentials` blob, precisely so it is QUERYABLE: a background
|
|
5
|
+
-- job asks "which connections expire in the next hour" and refreshes them.
|
|
6
|
+
-- Nothing inside the ciphertext can be indexed or compared in SQL.
|
|
7
|
+
--
|
|
8
|
+
-- The status CHECK is widened with RECONNECT_REQUIRED, which is deliberately
|
|
9
|
+
-- distinct from FAILED: the remedy is reauthorizing (a button), not fixing
|
|
10
|
+
-- credentials (a form).
|
|
11
|
+
|
|
12
|
+
ALTER TABLE "payment_provider_configs"
|
|
13
|
+
ADD COLUMN "expires_at" TIMESTAMP(3);
|
|
14
|
+
|
|
15
|
+
ALTER TABLE "payment_provider_configs"
|
|
16
|
+
DROP CONSTRAINT "payment_provider_configs_status_check";
|
|
17
|
+
|
|
18
|
+
ALTER TABLE "payment_provider_configs"
|
|
19
|
+
ADD CONSTRAINT "payment_provider_configs_status_check"
|
|
20
|
+
CHECK ("status" IN ('UNVERIFIED', 'VERIFIED', 'FAILED', 'RECONNECT_REQUIRED'));
|
|
21
|
+
|
|
22
|
+
-- Drives the refresh sweep: only connections that actually expire (OAuth)
|
|
23
|
+
-- have a non-null value, so this index stays small.
|
|
24
|
+
CREATE INDEX "payment_provider_configs_expires_at_idx"
|
|
25
|
+
ON "payment_provider_configs"("expires_at") WHERE "expires_at" IS NOT NULL;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- FUT-138: tenant-authored custom reports — the report-builder JSON ReportSpec
|
|
2
|
+
-- persisted as data. Owned by @12-apps/report-builder (this migration reaches the
|
|
3
|
+
-- host's migrations folder as a committed symlink). Unique per (client, name)
|
|
4
|
+
-- so MCP authoring can upsert by name. Deliberately NO foreign key into host
|
|
5
|
+
-- tables (self-contained package schema, payments-backend doctrine): the host
|
|
6
|
+
-- repository layer scopes every read/write by client_id.
|
|
7
|
+
CREATE TABLE "saved_reports" (
|
|
8
|
+
"id" TEXT NOT NULL,
|
|
9
|
+
"client_id" TEXT NOT NULL,
|
|
10
|
+
"name" TEXT NOT NULL,
|
|
11
|
+
"description" TEXT,
|
|
12
|
+
"spec" JSONB NOT NULL,
|
|
13
|
+
"created_by" TEXT,
|
|
14
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
16
|
+
|
|
17
|
+
CONSTRAINT "saved_reports_pkey" PRIMARY KEY ("id")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE UNIQUE INDEX "saved_reports_client_id_name_key" ON "saved_reports"("client_id", "name");
|
|
21
|
+
|
|
22
|
+
CREATE INDEX "saved_reports_client_id_idx" ON "saved_reports"("client_id");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: priority list + failover ledger.
|
|
2
|
+
--
|
|
3
|
+
-- This retires the SINGLE ACTIVE PROVIDER invariant. Until now the database
|
|
4
|
+
-- itself guaranteed at most one enabled config per merchant; a merchant may
|
|
5
|
+
-- now enable several and RANK them, and checkout walks that ranking when a
|
|
6
|
+
-- provider fails. The guarantee is not dropped so much as replaced: ranks
|
|
7
|
+
-- among a merchant's enabled rows must stay DISTINCT, or routing order would
|
|
8
|
+
-- again depend on row order — exactly the ambiguity the old index existed to
|
|
9
|
+
-- prevent.
|
|
10
|
+
|
|
11
|
+
ALTER TABLE "payment_provider_configs"
|
|
12
|
+
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
|
|
13
|
+
|
|
14
|
+
-- Backfill is a no-op by construction: the index being dropped below allowed
|
|
15
|
+
-- at most ONE enabled row per merchant, so every pre-existing chain has
|
|
16
|
+
-- exactly one member and rank 0 is already correct for it. Disabled rows also
|
|
17
|
+
-- carry 0, which the partial index below simply ignores.
|
|
18
|
+
|
|
19
|
+
DROP INDEX "payment_provider_configs_one_enabled_per_merchant";
|
|
20
|
+
|
|
21
|
+
-- The replacement invariant: two ENABLED providers can never share a rank, so
|
|
22
|
+
-- a merchant's chain is a total order. A racing reorder fails loudly here
|
|
23
|
+
-- rather than silently producing a chain whose order depends on which row the
|
|
24
|
+
-- planner returned first.
|
|
25
|
+
CREATE UNIQUE INDEX "payment_provider_configs_enabled_priority_key"
|
|
26
|
+
ON "payment_provider_configs"("merchant_kind", "merchant_id", "priority")
|
|
27
|
+
WHERE "enabled";
|
|
28
|
+
|
|
29
|
+
-- One row per ATTEMPT in a failover walk. This is the evidence trail for the
|
|
30
|
+
-- proof-of-failure rule: it records not just that a provider failed but which
|
|
31
|
+
-- BUCKET the failure fell into and, when the outcome was ambiguous, what the
|
|
32
|
+
-- reconciliation probe concluded. A double charge and a stranded charge look
|
|
33
|
+
-- identical in provider dashboards; they do not look identical here.
|
|
34
|
+
CREATE TABLE "payment_charge_attempts" (
|
|
35
|
+
"id" TEXT NOT NULL,
|
|
36
|
+
"merchant_kind" TEXT NOT NULL,
|
|
37
|
+
"merchant_id" TEXT NOT NULL,
|
|
38
|
+
"idempotency_key" TEXT,
|
|
39
|
+
"reference" TEXT NOT NULL,
|
|
40
|
+
"provider" TEXT NOT NULL,
|
|
41
|
+
"attempt_no" INTEGER NOT NULL,
|
|
42
|
+
"outcome" TEXT NOT NULL,
|
|
43
|
+
"failure_bucket" TEXT,
|
|
44
|
+
"probe_result" TEXT,
|
|
45
|
+
"provider_charge_id" TEXT,
|
|
46
|
+
"error" TEXT,
|
|
47
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
|
|
49
|
+
CONSTRAINT "payment_charge_attempts_pkey" PRIMARY KEY ("id"),
|
|
50
|
+
CONSTRAINT "payment_charge_attempts_merchant_kind_check"
|
|
51
|
+
CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
|
|
52
|
+
CONSTRAINT "payment_charge_attempts_outcome_check"
|
|
53
|
+
CHECK ("outcome" IN ('SUCCEEDED', 'ADOPTED', 'FAILED_OVER', 'STOPPED', 'DECLINED', 'SKIPPED')),
|
|
54
|
+
CONSTRAINT "payment_charge_attempts_failure_bucket_check"
|
|
55
|
+
CHECK ("failure_bucket" IS NULL OR "failure_bucket" IN ('DEFINITELY_NOT_CHARGED', 'AMBIGUOUS', 'BUSINESS_OUTCOME')),
|
|
56
|
+
CONSTRAINT "payment_charge_attempts_probe_result_check"
|
|
57
|
+
CHECK ("probe_result" IS NULL OR "probe_result" IN ('NOT_CHARGED', 'CHARGE_FOUND', 'PROBE_FAILED', 'UNSUPPORTED'))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- Resume lookup: "what has this idempotency key already tried?" — so a
|
|
61
|
+
-- retried charge continues the walk instead of restarting it.
|
|
62
|
+
CREATE INDEX "payment_charge_attempts_merchant_idempotency_key_idx"
|
|
63
|
+
ON "payment_charge_attempts"("merchant_kind", "merchant_id", "idempotency_key");
|
|
64
|
+
CREATE INDEX "payment_charge_attempts_reference_idx"
|
|
65
|
+
ON "payment_charge_attempts"("reference");
|
|
66
|
+
CREATE INDEX "payment_charge_attempts_merchant_created_at_idx"
|
|
67
|
+
ON "payment_charge_attempts"("merchant_kind", "merchant_id", "created_at");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: merchant-level failover policy.
|
|
2
|
+
--
|
|
3
|
+
-- FUT-303 shipped `failoverPolicy` as a construction-time gateway option, which
|
|
4
|
+
-- meant a merchant could not actually choose it — the ticket asks for a policy,
|
|
5
|
+
-- and a constant no store owner can reach is not one. This gives it a home.
|
|
6
|
+
--
|
|
7
|
+
-- It is merchant-level rather than per-provider on purpose: "may a decline
|
|
8
|
+
-- cascade to the next acquirer" is a decision about the CHAIN, and hanging it
|
|
9
|
+
-- off each provider row would let one merchant hold two contradictory answers.
|
|
10
|
+
|
|
11
|
+
CREATE TABLE "payment_merchant_settings" (
|
|
12
|
+
"id" TEXT NOT NULL,
|
|
13
|
+
"merchant_kind" TEXT NOT NULL,
|
|
14
|
+
"merchant_id" TEXT NOT NULL,
|
|
15
|
+
-- TECHNICAL is the default everywhere, including for merchants who never
|
|
16
|
+
-- get a row: cascading a declined card is opt-in, never inherited.
|
|
17
|
+
"failover_policy" TEXT NOT NULL DEFAULT 'TECHNICAL',
|
|
18
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
20
|
+
|
|
21
|
+
CONSTRAINT "payment_merchant_settings_pkey" PRIMARY KEY ("id"),
|
|
22
|
+
CONSTRAINT "payment_merchant_settings_merchant_kind_check"
|
|
23
|
+
CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
|
|
24
|
+
CONSTRAINT "payment_merchant_settings_failover_policy_check"
|
|
25
|
+
CHECK ("failover_policy" IN ('TECHNICAL', 'TECHNICAL_AND_DECLINE'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE UNIQUE INDEX "payment_merchant_settings_merchant_key"
|
|
29
|
+
ON "payment_merchant_settings"("merchant_kind", "merchant_id");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Report lifecycle & sharing (FUT-307): status (draft | published), visibility
|
|
2
|
+
-- (tenant | roles | private) and the role-id allowlist for visibility='roles'.
|
|
3
|
+
-- String + DB CHECK, the house style (no Prisma enums).
|
|
4
|
+
ALTER TABLE "saved_reports"
|
|
5
|
+
ADD COLUMN "status" TEXT NOT NULL DEFAULT 'draft',
|
|
6
|
+
ADD COLUMN "visibility" TEXT NOT NULL DEFAULT 'tenant',
|
|
7
|
+
ADD COLUMN "visibility_roles" JSONB NOT NULL DEFAULT '[]';
|
|
8
|
+
|
|
9
|
+
ALTER TABLE "saved_reports"
|
|
10
|
+
ADD CONSTRAINT "saved_reports_status_check"
|
|
11
|
+
CHECK ("status" IN ('draft', 'published'));
|
|
12
|
+
ALTER TABLE "saved_reports"
|
|
13
|
+
ADD CONSTRAINT "saved_reports_visibility_check"
|
|
14
|
+
CHECK ("visibility" IN ('tenant', 'roles', 'private'));
|
|
15
|
+
|
|
16
|
+
-- Every pre-lifecycle report is live today: backfill as published so nothing
|
|
17
|
+
-- disappears from any tenant's Relatórios area (new rows default to draft).
|
|
18
|
+
UPDATE "saved_reports" SET "status" = 'published';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Archived reports (FUT-391): the Relatórios area retires a report instead of
|
|
2
|
+
-- deleting it — the picker stops offering it, the document (and every block
|
|
3
|
+
-- spec in it) survives for restore. Widen the lifecycle CHECK to admit it.
|
|
4
|
+
ALTER TABLE "saved_reports"
|
|
5
|
+
DROP CONSTRAINT IF EXISTS "saved_reports_status_check";
|
|
6
|
+
|
|
7
|
+
ALTER TABLE "saved_reports"
|
|
8
|
+
ADD CONSTRAINT "saved_reports_status_check"
|
|
9
|
+
CHECK ("status" IN ('draft', 'published', 'archived'));
|