@fayz-ai/db 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +788 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/migrations/197_a_unit_has_its_own_currency.sql +93 -0
- package/migrations/198_permission_holes_close.sql +136 -0
- package/migrations/199_field_rules_speak_the_crud_entity.sql +82 -0
- package/migrations/200_team_members_have_a_name.sql +20 -0
- package/migrations/201_paid_features_are_facets.sql +39 -0
- package/migrations/202_a_unit_switches_off_a_module.sql +173 -0
- package/migrations/203_delivery_is_a_module.sql +31 -0
- package/migrations/204_a_module_off_in_a_unit_does_not_write_there.sql +95 -0
- package/migrations/205_v1_settings_screens_save.sql +833 -0
- package/migrations/206_the_account_access_has_one_screen.sql +25 -0
- package/migrations/207_a_line_answers_to_its_document.sql +65 -0
- package/migrations/208_a_service_opens_and_closes_per_unit.sql +18 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { Check, CheckBuilder, DefaultViewBuilderCore, ExtraConfigColumn, ForeignKey, ForeignKeyBuilder, Index, IndexBuilder, IndexBuilderOn, IndexedColumn, ManualMaterializedViewBuilder, ManualViewBuilder, MaterializedViewBuilder, MaterializedViewBuilderCore, PgArray, PgArrayBuilder, PgBigInt53, PgBigInt53Builder, PgBigInt64, PgBigInt64Builder, PgBigSerial53, PgBigSerial53Builder, PgBigSerial64, PgBigSerial64Builder, PgBinaryVector, PgBinaryVectorBuilder, PgBoolean, PgBooleanBuilder, PgChar, PgCharBuilder, PgCidr, PgCidrBuilder, PgColumn, PgColumnBuilder, PgCustomColumn, PgCustomColumnBuilder, PgDatabase, PgDate, PgDateBuilder, PgDateString, PgDateStringBuilder, PgDeleteBase, PgDialect, PgDoublePrecision, PgDoublePrecisionBuilder, PgEnumColumn, PgEnumColumnBuilder, PgGeometry, PgGeometryBuilder, PgGeometryObject, PgGeometryObjectBuilder, PgHalfVector, PgHalfVectorBuilder, PgInet, PgInetBuilder, PgInsertBase, PgInsertBuilder, PgIntColumnBaseBuilder, PgInteger, PgIntegerBuilder, PgInterval, PgIntervalBuilder, PgJson, PgJsonBuilder, PgJsonb, PgJsonbBuilder, PgLineABC, PgLineABCBuilder, PgLineBuilder, PgLineTuple, PgMacaddr, PgMacaddr8, PgMacaddr8Builder, PgMacaddrBuilder, PgMaterializedView, PgMaterializedViewConfig, PgNumeric, PgNumericBuilder, PgPointObject, PgPointObjectBuilder, PgPointTuple, PgPointTupleBuilder, PgPolicy, PgPreparedQuery, PgReal, PgRealBuilder, PgRefreshMaterializedView, PgRole, PgSchema, PgSelectBase, PgSelectBuilder, PgSelectQueryBuilderBase, PgSequence, PgSerial, PgSerialBuilder, PgSession, PgSmallInt, PgSmallIntBuilder, PgSmallSerial, PgSmallSerialBuilder, PgSparseVector, PgSparseVectorBuilder, PgTable, PgText, PgTextBuilder, PgTime, PgTimeBuilder, PgTimestamp, PgTimestampBuilder, PgTimestampString, PgTimestampStringBuilder, PgTransaction, PgUUID, PgUUIDBuilder, PgUpdateBase, PgUpdateBuilder, PgVarchar, PgVarcharBuilder, PgVector, PgVectorBuilder, PgView, PgViewConfig, PrimaryKey, PrimaryKeyBuilder, QueryBuilder, UniqueConstraint, UniqueConstraintBuilder, UniqueOnConstraintBuilder, ViewBuilder, alias, bigint, bigserial, bit, boolean, char, check, cidr, customType, date, decimal, doublePrecision, except, exceptAll, foreignKey, geometry, getMaterializedViewConfig, getTableConfig, getViewConfig, halfvec, index, inet, integer, intersect, intersectAll, interval, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, json, jsonb, line, macaddr, macaddr8, makePgArray, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, point, primaryKey, real, serial, smallint, smallserial, sparsevec, text, time, timestamp, union, unionAll, unique, uniqueIndex, uniqueKeyName, uuid, varchar, vector, withReplicas, } from 'drizzle-orm/pg-core';
|
|
2
|
+
export type { AnyForeignKeyBuilder, AnyIndexBuilder, AnyPgColumn, AnyPgDeleteBase, AnyPgInsert, AnyPgSelect, AnyPgSelectQueryBuilder, AnyPgSetOperatorInterface, AnyPgTable, AnyPgUpdate, BuildAliasTable, ColumnsWithTable, ConvertCustomConfig, CreatePgSelectFromBuilderMode, CustomTypeParams, CustomTypeValues, GetColumnsTableName, GetPgSetOperators, IndexColumn, IndexedExtraConfigType, IntervalConfig, LockConfig, LockStrength, PgBigInt53BuilderInitial, PgBigInt64BuilderInitial, PgBigIntConfig, PgBigSerial53BuilderInitial, PgBigSerial64BuilderInitial, PgBigSerialConfig, PgBinaryVectorBuilderInitial, PgBinaryVectorConfig, PgBooleanBuilderInitial, PgCharBuilderInitial, PgCharConfig, PgCidrBuilderInitial, PgColumnBuilderBase, PgCreateSetOperatorFn, PgCustomColumnInnerConfig, PgDateBuilderInitial, PgDateConfig, PgDateStringBuilderInitial, PgDelete, PgDeleteConfig, PgDeleteDynamic, PgDeletePrepare, PgDeleteReturning, PgDeleteReturningAll, PgDeleteWithout, PgDialectConfig, PgDoublePrecisionBuilderInitial, PgEnum, PgEnumColumnBuilderInitial, PgGeometryBuilderInitial, PgGeometryConfig, PgGeometryObjectBuilderInitial, PgHalfVectorBuilderInitial, PgHalfVectorConfig, PgIndexMethod, PgIndexOpClass, PgInetBuilderInitial, PgInsert, PgInsertConfig, PgInsertDynamic, PgInsertOnConflictDoUpdateConfig, PgInsertPrepare, PgInsertReturning, PgInsertReturningAll, PgInsertSelectQueryBuilder, PgInsertValue, PgInsertWithout, PgIntegerBuilderInitial, PgIntervalBuilderInitial, PgJsonBuilderInitial, PgJsonbBuilderInitial, PgLineABCBuilderInitial, PgLineBuilderInitial, PgLineTypeConfig, PgMacaddr8BuilderInitial, PgMacaddrBuilderInitial, PgMaterializedViewWithConfig, PgMaterializedViewWithSelection, PgNumericBuilderInitial, PgNumericConfig, PgPointConfig, PgPointObjectBuilderInitial, PgPointTupleBuilderInitial, PgPolicyConfig, PgPolicyToOption, PgQueryResultHKT, PgQueryResultKind, PgRealBuilderInitial, PgRoleConfig, PgSelect, PgSelectConfig, PgSelectDynamic, PgSelectHKT, PgSelectHKTBase, PgSelectJoin, PgSelectJoinConfig, PgSelectJoinFn, PgSelectKind, PgSelectPrepare, PgSelectQueryBuilder, PgSelectQueryBuilderHKT, PgSelectWithout, PgSequenceOptions, PgSerialBuilderInitial, PgSetOperator, PgSetOperatorExcludedMethods, PgSetOperatorInterface, PgSetOperatorWithResult, PgSmallIntBuilderInitial, PgSmallSerialBuilderInitial, PgSparseVectorBuilderInitial, PgSparseVectorConfig, PgTableExtraConfig, PgTableExtraConfigValue, PgTableFn, PgTableWithColumns, PgTextConfig, PgTimeBuilderInitial, PgTimestampBuilderInitial, PgTimestampConfig, PgTimestampStringBuilderInitial, PgTransactionConfig, PgUUIDBuilderInitial, PgUpdate, PgUpdateConfig, PgUpdateDynamic, PgUpdateJoin, PgUpdateJoinFn, PgUpdatePrepare, PgUpdateReturning, PgUpdateReturningAll, PgUpdateSetSource, PgUpdateWithJoins, PgUpdateWithout, PgVarcharBuilderInitial, PgVarcharConfig, PgVectorBuilderInitial, PgVectorConfig, PgViewWithSelection, PgWithReplicas, Precision, PreparedQueryConfig, Reference, ReferenceConfig, SelectedFields, SelectedFieldsFlat, SelectedFieldsOrdered, SetOperatorRestSelect, SetOperatorRightSelect, SubqueryWithSelection, TableConfig, TimeConfig, UpdateDeleteAction, ViewWithConfig, WithSubqueryWithSelection, } from 'drizzle-orm/pg-core';
|
|
2
3
|
export { tenants, people, orders, appointments, products, items, plgConnections, plgSyncRuns } from './schema/spine';
|
|
3
4
|
export { tenantId, timestamps, createdAt } from './lib/helpers';
|
|
4
5
|
//# 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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pgTable, uuid, timestamp, jsonb, text, boolean, integer } from 'drizzle-orm/pg-core';
|
|
2
|
-
export
|
|
2
|
+
export { Check, CheckBuilder, DefaultViewBuilderCore, ExtraConfigColumn, ForeignKey, ForeignKeyBuilder, Index, IndexBuilder, IndexBuilderOn, IndexedColumn, ManualMaterializedViewBuilder, ManualViewBuilder, MaterializedViewBuilder, MaterializedViewBuilderCore, PgArray, PgArrayBuilder, PgBigInt53, PgBigInt53Builder, PgBigInt64, PgBigInt64Builder, PgBigSerial53, PgBigSerial53Builder, PgBigSerial64, PgBigSerial64Builder, PgBinaryVector, PgBinaryVectorBuilder, PgBoolean, PgBooleanBuilder, PgChar, PgCharBuilder, PgCidr, PgCidrBuilder, PgColumn, PgColumnBuilder, PgCustomColumn, PgCustomColumnBuilder, PgDatabase, PgDate, PgDateBuilder, PgDateString, PgDateStringBuilder, PgDeleteBase, PgDialect, PgDoublePrecision, PgDoublePrecisionBuilder, PgEnumColumn, PgEnumColumnBuilder, PgGeometry, PgGeometryBuilder, PgGeometryObject, PgGeometryObjectBuilder, PgHalfVector, PgHalfVectorBuilder, PgInet, PgInetBuilder, PgInsertBase, PgInsertBuilder, PgIntColumnBaseBuilder, PgInteger, PgIntegerBuilder, PgInterval, PgIntervalBuilder, PgJson, PgJsonBuilder, PgJsonb, PgJsonbBuilder, PgLineABC, PgLineABCBuilder, PgLineBuilder, PgLineTuple, PgMacaddr, PgMacaddr8, PgMacaddr8Builder, PgMacaddrBuilder, PgMaterializedView, PgMaterializedViewConfig, PgNumeric, PgNumericBuilder, PgPointObject, PgPointObjectBuilder, PgPointTuple, PgPointTupleBuilder, PgPolicy, PgPreparedQuery, PgReal, PgRealBuilder, PgRefreshMaterializedView, PgRole, PgSchema, PgSelectBase, PgSelectBuilder, PgSelectQueryBuilderBase, PgSequence, PgSerial, PgSerialBuilder, PgSession, PgSmallInt, PgSmallIntBuilder, PgSmallSerial, PgSmallSerialBuilder, PgSparseVector, PgSparseVectorBuilder, PgTable, PgText, PgTextBuilder, PgTime, PgTimeBuilder, PgTimestamp, PgTimestampBuilder, PgTimestampString, PgTimestampStringBuilder, PgTransaction, PgUUID, PgUUIDBuilder, PgUpdateBase, PgUpdateBuilder, PgVarchar, PgVarcharBuilder, PgVector, PgVectorBuilder, PgView, PgViewConfig, PrimaryKey, PrimaryKeyBuilder, QueryBuilder, UniqueConstraint, UniqueConstraintBuilder, UniqueOnConstraintBuilder, ViewBuilder, alias, bigint, bigserial, bit, boolean, char, check, cidr, customType, date, decimal, doublePrecision, except, exceptAll, foreignKey, geometry, getMaterializedViewConfig, getTableConfig, getViewConfig, halfvec, index, inet, integer, intersect, intersectAll, interval, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, json, jsonb, line, macaddr, macaddr8, makePgArray, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, point, primaryKey, real, serial, smallint, smallserial, sparsevec, text, time, timestamp, union, unionAll, unique, uniqueIndex, uniqueKeyName, uuid, varchar, vector, withReplicas } from 'drizzle-orm/pg-core';
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
var tenants = pgTable("tenants", {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 197 — a unit has its own currency
|
|
3
|
+
--
|
|
4
|
+
-- One company, two shops: Rio sells in BRL, Barcelona in EUR. The `currency`
|
|
5
|
+
-- setting already existed, but only for the whole tenant, and nothing read it:
|
|
6
|
+
-- every order was born 'BRL' from a column default.
|
|
7
|
+
--
|
|
8
|
+
-- The currency is now a UNIT setting (the tenant's value stays the fallback,
|
|
9
|
+
-- which is what "the head office's currency" means), and an order with no
|
|
10
|
+
-- currency takes its unit's. A document keeps the currency it was written in:
|
|
11
|
+
-- changing the shop's currency later does not rewrite what was already sold.
|
|
12
|
+
--
|
|
13
|
+
-- A price list already carries its currency, so "one price per currency" is a
|
|
14
|
+
-- price list per currency. What it could not do was price a variant, and a
|
|
15
|
+
-- shirt is sold by size.
|
|
16
|
+
-- ============================================================================
|
|
17
|
+
|
|
18
|
+
SELECT app.seed_setting_keys($$[
|
|
19
|
+
{ "key": "currency", "type": "string", "default": "BRL", "scope": "unit",
|
|
20
|
+
"description": "Currency this unit sells, buys and keeps its books in (ISO 4217). The tenant's value is the default for every unit." }
|
|
21
|
+
]$$::jsonb);
|
|
22
|
+
|
|
23
|
+
CREATE OR REPLACE FUNCTION app.unit_currency(p_tenant uuid, p_unit uuid)
|
|
24
|
+
RETURNS text
|
|
25
|
+
LANGUAGE sql
|
|
26
|
+
STABLE SECURITY DEFINER
|
|
27
|
+
SET search_path TO ''
|
|
28
|
+
AS $function$
|
|
29
|
+
SELECT upper(coalesce(nullif(btrim(app.unit_setting_value(p_tenant, p_unit, 'currency') #>> '{}'), ''), 'BRL'));
|
|
30
|
+
$function$;
|
|
31
|
+
-- Takes the tenant as an argument, so it is not a client door.
|
|
32
|
+
REVOKE ALL ON FUNCTION app.unit_currency(uuid, uuid) FROM PUBLIC, anon, authenticated;
|
|
33
|
+
GRANT EXECUTE ON FUNCTION app.unit_currency(uuid, uuid) TO service_role;
|
|
34
|
+
|
|
35
|
+
-- What the signed-in user asks: "which currency is this unit of mine in?"
|
|
36
|
+
CREATE OR REPLACE FUNCTION public.my_unit_currency(p_unit uuid DEFAULT NULL)
|
|
37
|
+
RETURNS text
|
|
38
|
+
LANGUAGE sql
|
|
39
|
+
STABLE SECURITY DEFINER
|
|
40
|
+
SET search_path TO ''
|
|
41
|
+
AS $function$
|
|
42
|
+
SELECT app.unit_currency(app.current_tenant_id(), p_unit);
|
|
43
|
+
$function$;
|
|
44
|
+
REVOKE ALL ON FUNCTION public.my_unit_currency(uuid) FROM PUBLIC, anon;
|
|
45
|
+
GRANT EXECUTE ON FUNCTION public.my_unit_currency(uuid) TO authenticated, service_role;
|
|
46
|
+
|
|
47
|
+
-- ── an order is born in its unit's currency ────────────────────────────────
|
|
48
|
+
ALTER TABLE public.orders ALTER COLUMN currency DROP DEFAULT;
|
|
49
|
+
|
|
50
|
+
CREATE OR REPLACE FUNCTION public.trg_orders_currency_default()
|
|
51
|
+
RETURNS trigger
|
|
52
|
+
LANGUAGE plpgsql
|
|
53
|
+
SECURITY DEFINER
|
|
54
|
+
SET search_path TO ''
|
|
55
|
+
AS $function$
|
|
56
|
+
BEGIN
|
|
57
|
+
IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
|
|
58
|
+
NEW.currency := app.unit_currency(NEW.tenant_id, NEW.unit_id);
|
|
59
|
+
ELSE
|
|
60
|
+
NEW.currency := upper(btrim(NEW.currency));
|
|
61
|
+
END IF;
|
|
62
|
+
RETURN NEW;
|
|
63
|
+
END $function$;
|
|
64
|
+
|
|
65
|
+
-- "zz": BEFORE triggers fire in name order, and the unit is stamped by
|
|
66
|
+
-- `orders_scope_stamp`. This one has to read the unit after that.
|
|
67
|
+
DROP TRIGGER IF EXISTS orders_zz_currency_default ON public.orders;
|
|
68
|
+
CREATE TRIGGER orders_zz_currency_default
|
|
69
|
+
BEFORE INSERT ON public.orders
|
|
70
|
+
FOR EACH ROW EXECUTE FUNCTION public.trg_orders_currency_default();
|
|
71
|
+
|
|
72
|
+
-- ── a price list can price a variant ───────────────────────────────────────
|
|
73
|
+
ALTER TABLE public.price_table_items
|
|
74
|
+
ADD COLUMN IF NOT EXISTS variant_id uuid REFERENCES public.product_variants(id) ON DELETE CASCADE;
|
|
75
|
+
COMMENT ON COLUMN public.price_table_items.variant_id IS
|
|
76
|
+
'Price for one variant when it differs; NULL = the price of the whole product.';
|
|
77
|
+
|
|
78
|
+
-- Same constraint, one more column: NULLS NOT DISTINCT keeps "one product-wide
|
|
79
|
+
-- price per quantity break" unique, which a plain UNIQUE would stop enforcing.
|
|
80
|
+
DO $$
|
|
81
|
+
BEGIN
|
|
82
|
+
IF NOT EXISTS (
|
|
83
|
+
SELECT 1 FROM pg_constraint c
|
|
84
|
+
WHERE c.conrelid = 'public.price_table_items'::regclass AND c.conname = 'price_table_items_unique'
|
|
85
|
+
AND pg_get_constraintdef(c.oid) LIKE '%variant_id%') THEN
|
|
86
|
+
ALTER TABLE public.price_table_items DROP CONSTRAINT IF EXISTS price_table_items_unique;
|
|
87
|
+
ALTER TABLE public.price_table_items
|
|
88
|
+
ADD CONSTRAINT price_table_items_unique
|
|
89
|
+
UNIQUE NULLS NOT DISTINCT (price_table_id, product_id, variant_id, min_qty);
|
|
90
|
+
END IF;
|
|
91
|
+
END $$;
|
|
92
|
+
CREATE INDEX IF NOT EXISTS price_table_items_variant_idx
|
|
93
|
+
ON public.price_table_items (tenant_id, variant_id) WHERE variant_id IS NOT NULL;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 198 — permission holes close
|
|
3
|
+
--
|
|
4
|
+
-- Three holes found while mapping V1 permissions onto V2:
|
|
5
|
+
--
|
|
6
|
+
-- 1. `role_set_permissions` checked only `roles.manage`. Admin holds it, so an
|
|
7
|
+
-- admin could hand `billing.*` / `admin.*` (owner_only, 029) to any role,
|
|
8
|
+
-- including their own. Now only the owner may GRANT an owner_only key;
|
|
9
|
+
-- keys a role already holds survive an edit (029: owner_only governs what is
|
|
10
|
+
-- granted from now on, it does not revoke).
|
|
11
|
+
--
|
|
12
|
+
-- 2. `public.rooms` (153) let any tenant member write. The screen is gated by
|
|
13
|
+
-- `services.*` (148), so the table is too. Reading stays open to members:
|
|
14
|
+
-- the agenda needs every room to place an appointment.
|
|
15
|
+
--
|
|
16
|
+
-- 3. Screens gated by keys nobody could hold — the catalog lacked them, so
|
|
17
|
+
-- only the owner (who reads the catalog) saw them, and `role_set_permissions`
|
|
18
|
+
-- refused them as unknown. `workforce.*` mirrors `workforce_hr.*` (same
|
|
19
|
+
-- audience: who runs the team's time); `reservations.*` mirrors `tables.*`
|
|
20
|
+
-- (who runs the floor). Mirroring grants nobody anything new in spirit.
|
|
21
|
+
--
|
|
22
|
+
-- Idempotent.
|
|
23
|
+
-- ============================================================================
|
|
24
|
+
|
|
25
|
+
CREATE OR REPLACE FUNCTION public.role_set_permissions(p_role_id uuid, p_keys text[]) RETURNS integer
|
|
26
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
27
|
+
SET search_path TO ''
|
|
28
|
+
AS $$
|
|
29
|
+
DECLARE
|
|
30
|
+
v_tenant uuid := app.current_tenant_id();
|
|
31
|
+
v_role app.roles%ROWTYPE;
|
|
32
|
+
v_unknown text;
|
|
33
|
+
v_reserved text;
|
|
34
|
+
v_keys text[] := coalesce(p_keys, '{}');
|
|
35
|
+
v_n integer;
|
|
36
|
+
BEGIN
|
|
37
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
38
|
+
RAISE EXCEPTION 'role_set_permissions: no active tenant for this session' USING ERRCODE = '42501';
|
|
39
|
+
END IF;
|
|
40
|
+
IF NOT app.has_permission('roles.manage') THEN
|
|
41
|
+
RAISE EXCEPTION 'role_set_permissions: roles.manage is required' USING ERRCODE = '42501';
|
|
42
|
+
END IF;
|
|
43
|
+
SELECT * INTO v_role FROM app.roles r WHERE r.tenant_id = v_tenant AND r.id = p_role_id;
|
|
44
|
+
IF NOT FOUND THEN
|
|
45
|
+
RAISE EXCEPTION 'role_set_permissions: role not found in the active tenant' USING ERRCODE = 'P0002';
|
|
46
|
+
END IF;
|
|
47
|
+
IF v_role.is_system AND v_role.key = 'owner' THEN
|
|
48
|
+
RAISE EXCEPTION 'role_set_permissions: the owner role is immutable (it always holds the whole catalog)' USING ERRCODE = '42501';
|
|
49
|
+
END IF;
|
|
50
|
+
SELECT string_agg(k, ', ' ORDER BY k) INTO v_unknown
|
|
51
|
+
FROM unnest(v_keys) k WHERE NOT EXISTS (SELECT 1 FROM app.permissions p WHERE p.key = k);
|
|
52
|
+
IF v_unknown IS NOT NULL THEN
|
|
53
|
+
RAISE EXCEPTION 'role_set_permissions: unknown permission key(s): %', v_unknown USING ERRCODE = '23503';
|
|
54
|
+
END IF;
|
|
55
|
+
|
|
56
|
+
IF NOT EXISTS (
|
|
57
|
+
SELECT 1 FROM app.memberships m
|
|
58
|
+
JOIN app.roles r ON r.id = m.role_id AND r.tenant_id = m.tenant_id
|
|
59
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.active AND r.key = 'owner'
|
|
60
|
+
) THEN
|
|
61
|
+
SELECT string_agg(k, ', ' ORDER BY k) INTO v_reserved
|
|
62
|
+
FROM unnest(v_keys) k
|
|
63
|
+
JOIN app.permissions p ON p.key = k AND p.owner_only
|
|
64
|
+
WHERE NOT EXISTS (
|
|
65
|
+
SELECT 1 FROM app.role_permissions rp
|
|
66
|
+
WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id AND rp.permission = k);
|
|
67
|
+
IF v_reserved IS NOT NULL THEN
|
|
68
|
+
RAISE EXCEPTION 'role_set_permissions: only the owner can grant %', v_reserved USING ERRCODE = '42501';
|
|
69
|
+
END IF;
|
|
70
|
+
END IF;
|
|
71
|
+
|
|
72
|
+
DELETE FROM app.role_permissions rp
|
|
73
|
+
WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id AND rp.permission <> ALL (v_keys);
|
|
74
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
75
|
+
SELECT v_tenant, p_role_id, k FROM unnest(v_keys) k
|
|
76
|
+
ON CONFLICT DO NOTHING;
|
|
77
|
+
|
|
78
|
+
SELECT count(*) INTO v_n FROM app.role_permissions rp WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id;
|
|
79
|
+
RETURN v_n;
|
|
80
|
+
END $$;
|
|
81
|
+
|
|
82
|
+
-- ── 2. rooms ────────────────────────────────────────────────────────────────
|
|
83
|
+
DROP POLICY IF EXISTS rooms_member_all ON public.rooms;
|
|
84
|
+
DROP POLICY IF EXISTS rooms_member_read ON public.rooms;
|
|
85
|
+
DROP POLICY IF EXISTS rooms_insert ON public.rooms;
|
|
86
|
+
DROP POLICY IF EXISTS rooms_update ON public.rooms;
|
|
87
|
+
DROP POLICY IF EXISTS rooms_delete ON public.rooms;
|
|
88
|
+
|
|
89
|
+
CREATE POLICY rooms_member_read ON public.rooms FOR SELECT TO authenticated
|
|
90
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
91
|
+
CREATE POLICY rooms_insert ON public.rooms FOR INSERT TO authenticated
|
|
92
|
+
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids())
|
|
93
|
+
AND (SELECT app.has_permission('services.create', unit_id)));
|
|
94
|
+
CREATE POLICY rooms_update ON public.rooms FOR UPDATE TO authenticated
|
|
95
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids())
|
|
96
|
+
AND (SELECT app.has_permission('services.edit', unit_id)))
|
|
97
|
+
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids())
|
|
98
|
+
AND (SELECT app.has_permission('services.edit', unit_id)));
|
|
99
|
+
CREATE POLICY rooms_delete ON public.rooms FOR DELETE TO authenticated
|
|
100
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids())
|
|
101
|
+
AND (SELECT app.has_permission('services.delete', unit_id)));
|
|
102
|
+
|
|
103
|
+
-- ── 3. keys the screens already ask for ─────────────────────────────────────
|
|
104
|
+
INSERT INTO app.permissions (key, plugin, description)
|
|
105
|
+
VALUES
|
|
106
|
+
('workforce.read', 'workforce', 'Ver escalas, jornadas e batidas de ponto'),
|
|
107
|
+
('workforce.create', 'workforce', 'Criar escala, turno ou lançamento de ponto'),
|
|
108
|
+
('workforce.edit', 'workforce', 'Alterar escala, turno ou lançamento de ponto'),
|
|
109
|
+
('workforce.delete', 'workforce', 'Excluir escala, turno ou lançamento de ponto'),
|
|
110
|
+
('workforce.manage', 'workforce', 'Coringa de escalas e ponto'),
|
|
111
|
+
('reservations.read', 'reservations', 'Ver reservas'),
|
|
112
|
+
('reservations.create', 'reservations', 'Registrar reserva'),
|
|
113
|
+
('reservations.edit', 'reservations', 'Alterar ou confirmar reserva'),
|
|
114
|
+
('reservations.delete', 'reservations', 'Cancelar ou excluir reserva'),
|
|
115
|
+
('reservations.manage', 'reservations', 'Coringa de reservas')
|
|
116
|
+
ON CONFLICT (key) DO NOTHING;
|
|
117
|
+
|
|
118
|
+
INSERT INTO app.role_permissions (role_id, permission, tenant_id)
|
|
119
|
+
SELECT p.role_id, replace(p.permission, 'workforce_hr.', 'workforce.'), p.tenant_id
|
|
120
|
+
FROM app.role_permissions p
|
|
121
|
+
WHERE p.permission IN ('workforce_hr.read', 'workforce_hr.create', 'workforce_hr.edit', 'workforce_hr.delete')
|
|
122
|
+
ON CONFLICT DO NOTHING;
|
|
123
|
+
|
|
124
|
+
INSERT INTO app.role_permissions (role_id, permission, tenant_id)
|
|
125
|
+
SELECT p.role_id, replace(p.permission, 'tables.', 'reservations.'), p.tenant_id
|
|
126
|
+
FROM app.role_permissions p
|
|
127
|
+
WHERE p.permission IN ('tables.read', 'tables.create', 'tables.edit', 'tables.delete', 'tables.manage')
|
|
128
|
+
ON CONFLICT DO NOTHING;
|
|
129
|
+
|
|
130
|
+
-- Administrator reads the catalog minus owner_only (029): new keys reach it too.
|
|
131
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
132
|
+
SELECT r.tenant_id, r.id, p.key
|
|
133
|
+
FROM app.roles r
|
|
134
|
+
JOIN app.permissions p ON p.key ~ '^(workforce|reservations)\.(read|create|edit|delete|manage)$'
|
|
135
|
+
WHERE r.key = 'admin' AND p.owner_only = false
|
|
136
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 199 — field rules speak the CRUD entity
|
|
3
|
+
--
|
|
4
|
+
-- V1's required_field_rules came across with two errors:
|
|
5
|
+
-- 1. the allowlist pinned state='required', so V1 rows that only said
|
|
6
|
+
-- "visible" landed as required (Espaço Facial: 8 required became 12);
|
|
7
|
+
-- 2. they kept V1's vocabulary — reference 'clients', field 'cpf' — which no
|
|
8
|
+
-- V2 screen reads. V2 keys a rule by the CRUD entity (archetype:kind,
|
|
9
|
+
-- stored snake: `person_customer`) and the entity's own FieldDef keys.
|
|
10
|
+
--
|
|
11
|
+
-- The writer now takes V1's `visibility` and translates entity and field on
|
|
12
|
+
-- the way in. A V1 field V2 has no place for is skipped with its reason
|
|
13
|
+
-- instead of landing as a rule nothing reads.
|
|
14
|
+
--
|
|
15
|
+
-- Rows already imported are corrected by a re-run of the field-rule import,
|
|
16
|
+
-- not here: the V1 values live in the V1 database.
|
|
17
|
+
--
|
|
18
|
+
-- Idempotent.
|
|
19
|
+
-- ============================================================================
|
|
20
|
+
|
|
21
|
+
DO $do$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF to_regprocedure('migration.upsert_field_rule_v1row(uuid, uuid, jsonb, jsonb)') IS NULL THEN
|
|
24
|
+
ALTER FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) RENAME TO upsert_field_rule_v1row;
|
|
25
|
+
END IF;
|
|
26
|
+
END $do$;
|
|
27
|
+
|
|
28
|
+
CREATE OR REPLACE FUNCTION migration.upsert_field_rule(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
29
|
+
RETURNS jsonb
|
|
30
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
31
|
+
SET search_path TO ''
|
|
32
|
+
AS $$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_entities jsonb := '{"clients":"person_customer","professionals":"person_staff","employees":"person_staff","suppliers":"person_supplier"}';
|
|
35
|
+
v_fields jsonb := '{
|
|
36
|
+
"person_customer": {"name":"name","cpf":"documentNumber","rg":"rg","birth_date":"dateOfBirth","gender":"gender",
|
|
37
|
+
"phone":"phone","mobile":"mobile","email":"email","social_name":"socialName",
|
|
38
|
+
"profession":"profession","origin":"originId","warnings":"warnings","address":"address"},
|
|
39
|
+
"person_staff": {"name":"name","cpf":"documentNumber","email":"email","phone":"phone",
|
|
40
|
+
"specialty":"profession","commission_rate":"commissionRate"},
|
|
41
|
+
"person_supplier": {"name":"name","cpf_cnpj":"documentNumber","phone":"phone","mobile":"mobile","email":"email","address":"address"}
|
|
42
|
+
}';
|
|
43
|
+
v_ref_in text := lower(nullif(btrim(coalesce(p_row ->> 'reference', '')), ''));
|
|
44
|
+
v_ref text := coalesce(v_entities ->> v_ref_in, v_ref_in);
|
|
45
|
+
v_key_in text := nullif(btrim(coalesce(p_row ->> 'field_key', '')), '');
|
|
46
|
+
v_key text;
|
|
47
|
+
BEGIN
|
|
48
|
+
IF v_fields ? v_ref THEN
|
|
49
|
+
v_key := v_fields -> v_ref ->> v_key_in;
|
|
50
|
+
IF v_key IS NULL THEN
|
|
51
|
+
RETURN jsonb_build_object('status', 'skipped', 'kind', 'value',
|
|
52
|
+
'reason', format('V1 field %L of %L has no field in the V2 %s form (V2 keeps the address as one field, ruled by `address`)', v_key_in, v_ref_in, v_ref));
|
|
53
|
+
END IF;
|
|
54
|
+
ELSE
|
|
55
|
+
v_key := v_key_in;
|
|
56
|
+
END IF;
|
|
57
|
+
RETURN migration.upsert_field_rule_v1row(
|
|
58
|
+
p_tenant, p_id,
|
|
59
|
+
p_row || jsonb_build_object('reference', v_ref, 'field_key', v_key,
|
|
60
|
+
'state', coalesce(p_row ->> 'visibility', p_row ->> 'state', p_args ->> 'state', p_args ->> 'default_state')),
|
|
61
|
+
p_args - 'state' - 'default_state');
|
|
62
|
+
END $$;
|
|
63
|
+
|
|
64
|
+
REVOKE ALL ON FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) FROM PUBLIC;
|
|
65
|
+
GRANT ALL ON FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
66
|
+
REVOKE ALL ON FUNCTION migration.upsert_field_rule_v1row(uuid, uuid, jsonb, jsonb) FROM PUBLIC;
|
|
67
|
+
|
|
68
|
+
DO $unlock$
|
|
69
|
+
BEGIN
|
|
70
|
+
PERFORM set_config('migration.allowlist_unlock', 'on', true);
|
|
71
|
+
-- A V1 row with no `visibility` (older exports) still means required.
|
|
72
|
+
PERFORM migration._allow('required_field_rules', 'public.field_rules', 'config', 'field_rule', '{"default_state":"required"}', '{}',
|
|
73
|
+
'{"uuid":"id","context":"context","entity":"reference","field_key":"field_key","visibility":"visibility","unit_id":"unit_id@companies"}',
|
|
74
|
+
'199: V1 `visibility` is carried, not pinned to required; entity and field are translated to the V2 CRUD entity by the writer.');
|
|
75
|
+
END $unlock$;
|
|
76
|
+
|
|
77
|
+
-- V1's two form contexts. The live cluster already has them (its rules use
|
|
78
|
+
-- them); the chain did not.
|
|
79
|
+
INSERT INTO public.field_rule_contexts (key, plugin, description) VALUES
|
|
80
|
+
('main_form', 'base', 'The full registry form (V1 "Cadastro principal")'),
|
|
81
|
+
('quick_search', 'base', 'The quick add from a search box (V1 "Busca rápida")')
|
|
82
|
+
ON CONFLICT (key) DO NOTHING;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 200 — team members have a name
|
|
3
|
+
--
|
|
4
|
+
-- Choosing "who can authorize" needs a list of logins with names. Memberships
|
|
5
|
+
-- carry the login, people carry the name, and no readable view joined the two:
|
|
6
|
+
-- v_team_members has user_id and person_id but no name.
|
|
7
|
+
--
|
|
8
|
+
-- security_invoker: it reads with the caller's own RLS on people and
|
|
9
|
+
-- memberships, so it shows exactly the members the caller may already see.
|
|
10
|
+
--
|
|
11
|
+
-- Idempotent.
|
|
12
|
+
-- ============================================================================
|
|
13
|
+
CREATE OR REPLACE VIEW public.v_team_member_names
|
|
14
|
+
WITH (security_invoker = true) AS
|
|
15
|
+
SELECT m.tenant_id, m.user_id, m.person_id, coalesce(nullif(p.name, ''), 'Sem nome') AS name
|
|
16
|
+
FROM app.memberships m
|
|
17
|
+
LEFT JOIN public.people p ON p.id = m.person_id
|
|
18
|
+
WHERE m.active;
|
|
19
|
+
|
|
20
|
+
GRANT SELECT ON public.v_team_member_names TO authenticated;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- 201: features that were an on/off setting become facets a plan sells apart
|
|
2
|
+
-- (time clock, AtendeAI, cashback club, kiosks, waitlist, table tablets, scale,
|
|
3
|
+
-- QR menu). A plan that already lists any grant denies an unlisted facet
|
|
4
|
+
-- (`plan_permits` reads silence per plan, not per facet — see 164), so each
|
|
5
|
+
-- facet is granted as `optional` wherever its plugin is granted: the account
|
|
6
|
+
-- may turn it on in Módulos, and pricing counts it once it does.
|
|
7
|
+
-- Additive and idempotent; plans that do not grant the plugin stay as they are.
|
|
8
|
+
|
|
9
|
+
DO $$
|
|
10
|
+
DECLARE
|
|
11
|
+
v record;
|
|
12
|
+
BEGIN
|
|
13
|
+
IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NULL THEN
|
|
14
|
+
RAISE NOTICE '201: register_plan_grant missing — skipped (pool without app.plan_grants)';
|
|
15
|
+
RETURN;
|
|
16
|
+
END IF;
|
|
17
|
+
|
|
18
|
+
FOR v IN
|
|
19
|
+
SELECT g.plan_id, f.plugin_id, f.facet
|
|
20
|
+
FROM (VALUES
|
|
21
|
+
('workforce', 'time_clock'),
|
|
22
|
+
('conversations', 'atendeai'),
|
|
23
|
+
('crm', 'loyalty'),
|
|
24
|
+
('agenda', 'kiosk'),
|
|
25
|
+
('tables', 'waitlist'),
|
|
26
|
+
('tables', 'tablets'),
|
|
27
|
+
('orders', 'scale'),
|
|
28
|
+
('menu', 'qr_menu')
|
|
29
|
+
) AS f(plugin_id, facet)
|
|
30
|
+
JOIN app.plan_grants g ON g.plugin_id = f.plugin_id AND g.facet = ''
|
|
31
|
+
WHERE NOT EXISTS (
|
|
32
|
+
SELECT 1 FROM app.plan_grants x
|
|
33
|
+
WHERE x.plan_id = g.plan_id AND x.plugin_id = f.plugin_id AND x.facet = f.facet
|
|
34
|
+
)
|
|
35
|
+
LOOP
|
|
36
|
+
PERFORM public.register_plan_grant(v.plan_id, v.plugin_id, v.facet, 'optional',
|
|
37
|
+
'Was an on/off setting; now a paid part the account turns on in Módulos.');
|
|
38
|
+
END LOOP;
|
|
39
|
+
END $$;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
-- 202: a unit can switch a module off for itself. A network sells at the
|
|
2
|
+
-- counter in three units and not in its services unit: the account has Pedidos,
|
|
3
|
+
-- that unit does not. Absence inherits the account; only an explicit 'disabled' row
|
|
4
|
+
-- narrows, so a unit can never turn on what the account (and its plan) does not
|
|
5
|
+
-- have — which is why the write needs no plan check.
|
|
6
|
+
-- Additive and idempotent. app_config() is re-emitted from the FILE of its last
|
|
7
|
+
-- definition (138) with one more key; nothing else in it changes.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS app.unit_plugins (
|
|
10
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
11
|
+
unit_id uuid NOT NULL,
|
|
12
|
+
plugin_id text NOT NULL CHECK (plugin_id ~ '^[a-z][a-z0-9-]*$'),
|
|
13
|
+
facet text NOT NULL DEFAULT '' CHECK (facet = '' OR facet ~ '^[a-z][a-z0-9_-]*$'),
|
|
14
|
+
status text NOT NULL DEFAULT 'disabled' CHECK (status IN ('active', 'disabled')),
|
|
15
|
+
updated_by uuid,
|
|
16
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
17
|
+
PRIMARY KEY (tenant_id, unit_id, plugin_id, facet),
|
|
18
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
ALTER TABLE app.unit_plugins ENABLE ROW LEVEL SECURITY;
|
|
22
|
+
ALTER TABLE app.unit_plugins FORCE ROW LEVEL SECURITY;
|
|
23
|
+
|
|
24
|
+
DO $$
|
|
25
|
+
BEGIN
|
|
26
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'app' AND tablename = 'unit_plugins' AND policyname = 'unit_plugins_read') THEN
|
|
27
|
+
-- Same reach as the account's own switches: every member renders the menu
|
|
28
|
+
-- of whichever unit they stand in. Writes go through unit_plugin_set only.
|
|
29
|
+
CREATE POLICY unit_plugins_read ON app.unit_plugins FOR SELECT TO authenticated
|
|
30
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
31
|
+
END IF;
|
|
32
|
+
END $$;
|
|
33
|
+
|
|
34
|
+
REVOKE ALL ON app.unit_plugins FROM PUBLIC, anon;
|
|
35
|
+
GRANT SELECT ON app.unit_plugins TO authenticated;
|
|
36
|
+
GRANT ALL ON app.unit_plugins TO service_role;
|
|
37
|
+
|
|
38
|
+
-- Whether a module (or one facet of it) may run in a unit of the caller's
|
|
39
|
+
-- account: not switched off by the account, nor by that unit. A plugin the
|
|
40
|
+
-- account never mentions counts as on — the manifest default — exactly as the
|
|
41
|
+
-- shell reads it. Meant for RLS and RPC guards.
|
|
42
|
+
CREATE OR REPLACE FUNCTION app.plugin_enabled_for_unit(p_plugin text, p_unit uuid, p_facet text DEFAULT '')
|
|
43
|
+
RETURNS boolean
|
|
44
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
45
|
+
SET search_path TO ''
|
|
46
|
+
AS $$
|
|
47
|
+
SELECT NOT EXISTS (
|
|
48
|
+
SELECT 1 FROM app.tenant_plugins t
|
|
49
|
+
WHERE t.tenant_id = app.current_tenant_id() AND t.plugin_id = p_plugin
|
|
50
|
+
AND t.facet IN ('', coalesce(p_facet, ''))
|
|
51
|
+
AND (t.status = 'disabled' OR (t.expires_at IS NOT NULL AND t.expires_at <= now())))
|
|
52
|
+
AND NOT EXISTS (
|
|
53
|
+
SELECT 1 FROM app.unit_plugins u
|
|
54
|
+
WHERE u.tenant_id = app.current_tenant_id() AND u.unit_id = p_unit AND u.plugin_id = p_plugin
|
|
55
|
+
AND u.facet IN ('', coalesce(p_facet, '')) AND u.status = 'disabled')
|
|
56
|
+
$$;
|
|
57
|
+
REVOKE ALL ON FUNCTION app.plugin_enabled_for_unit(text, uuid, text) FROM PUBLIC;
|
|
58
|
+
GRANT EXECUTE ON FUNCTION app.plugin_enabled_for_unit(text, uuid, text) TO authenticated, service_role;
|
|
59
|
+
|
|
60
|
+
-- The only door: switch a module off in one unit, or back to what the account has.
|
|
61
|
+
CREATE OR REPLACE FUNCTION public.unit_plugin_set(p_unit uuid, p_plugin text, p_facet text DEFAULT '', p_status text DEFAULT 'disabled')
|
|
62
|
+
RETURNS jsonb
|
|
63
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
64
|
+
SET search_path TO ''
|
|
65
|
+
AS $$
|
|
66
|
+
DECLARE
|
|
67
|
+
v_tenant uuid := app.current_tenant_id();
|
|
68
|
+
BEGIN
|
|
69
|
+
IF v_tenant IS NULL THEN
|
|
70
|
+
RAISE EXCEPTION 'no tenant in scope' USING ERRCODE = '42501';
|
|
71
|
+
END IF;
|
|
72
|
+
IF NOT app.has_permission('settings.manage') THEN
|
|
73
|
+
RAISE EXCEPTION 'settings.manage is required to change what a unit has' USING ERRCODE = '42501';
|
|
74
|
+
END IF;
|
|
75
|
+
IF p_status NOT IN ('active', 'disabled') THEN
|
|
76
|
+
RAISE EXCEPTION 'status must be active or disabled' USING ERRCODE = '22023';
|
|
77
|
+
END IF;
|
|
78
|
+
IF NOT EXISTS (SELECT 1 FROM app.units WHERE tenant_id = v_tenant AND id = p_unit) THEN
|
|
79
|
+
RAISE EXCEPTION 'unit % is not in this account', p_unit USING ERRCODE = '42501';
|
|
80
|
+
END IF;
|
|
81
|
+
|
|
82
|
+
IF p_status = 'active' THEN
|
|
83
|
+
-- Back to the account's choice: the row goes, the unit inherits.
|
|
84
|
+
DELETE FROM app.unit_plugins
|
|
85
|
+
WHERE tenant_id = v_tenant AND unit_id = p_unit AND plugin_id = p_plugin AND facet = coalesce(p_facet, '');
|
|
86
|
+
ELSE
|
|
87
|
+
INSERT INTO app.unit_plugins (tenant_id, unit_id, plugin_id, facet, status, updated_by)
|
|
88
|
+
VALUES (v_tenant, p_unit, p_plugin, coalesce(p_facet, ''), 'disabled', auth.uid())
|
|
89
|
+
ON CONFLICT (tenant_id, unit_id, plugin_id, facet)
|
|
90
|
+
DO UPDATE SET status = 'disabled', updated_by = EXCLUDED.updated_by, updated_at = now();
|
|
91
|
+
END IF;
|
|
92
|
+
|
|
93
|
+
RETURN jsonb_build_object('unit', p_unit, 'plugin', p_plugin, 'facet', nullif(coalesce(p_facet, ''), ''),
|
|
94
|
+
'status', CASE WHEN p_status = 'active' THEN 'inherited' ELSE 'disabled' END);
|
|
95
|
+
END $$;
|
|
96
|
+
REVOKE ALL ON FUNCTION public.unit_plugin_set(uuid, text, text, text) FROM PUBLIC;
|
|
97
|
+
GRANT EXECUTE ON FUNCTION public.unit_plugin_set(uuid, text, text, text) TO authenticated, service_role;
|
|
98
|
+
|
|
99
|
+
CREATE OR REPLACE FUNCTION public.app_config()
|
|
100
|
+
RETURNS jsonb
|
|
101
|
+
LANGUAGE plpgsql
|
|
102
|
+
STABLE SECURITY DEFINER
|
|
103
|
+
SET search_path TO ''
|
|
104
|
+
AS $function$
|
|
105
|
+
DECLARE
|
|
106
|
+
v_tenant uuid := app.current_tenant_id();
|
|
107
|
+
v_staff text;
|
|
108
|
+
BEGIN
|
|
109
|
+
-- `platform` é do CLUSTER, não da cadeia canônica: um banco montado do zero
|
|
110
|
+
-- (o db-from-zero, um pool novo) não tem esse schema, e uma chamada direta
|
|
111
|
+
-- estoura na inicialização da variável — antes de qualquer linha do corpo.
|
|
112
|
+
IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
|
|
113
|
+
EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
|
|
114
|
+
END IF;
|
|
115
|
+
|
|
116
|
+
IF v_tenant IS NULL THEN
|
|
117
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb,
|
|
118
|
+
'apps', '[]'::jsonb, 'permissions', '[]'::jsonb, 'unit_plugins', '[]'::jsonb, 'generated_at', now());
|
|
119
|
+
END IF;
|
|
120
|
+
|
|
121
|
+
RETURN jsonb_build_object(
|
|
122
|
+
'tenant_id', v_tenant,
|
|
123
|
+
'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
|
|
124
|
+
'settings', public.tenant_settings(),
|
|
125
|
+
'plugins', coalesce((
|
|
126
|
+
SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
|
|
127
|
+
'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
|
|
128
|
+
'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
|
|
129
|
+
ORDER BY t.plugin_id, t.facet)
|
|
130
|
+
FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
|
|
131
|
+
-- `trade` ordena a lista: quem roda dois aplicativos tem UM ofício, e o
|
|
132
|
+
-- outro é retaguarda (084). Entre dois SEM ofício, quem declara
|
|
133
|
+
-- `preset.rail` vence quem não declara (115).
|
|
134
|
+
--
|
|
135
|
+
-- O PRIMEIRO desta lista é quem empresta o rail (`useAppPreset`) e agora
|
|
136
|
+
-- também quem empresta a COR (`useAppTheme`) — as duas leituras têm de ser
|
|
137
|
+
-- o mesmo aplicativo, senão a casa abre com o menu de um produto e a cara
|
|
138
|
+
-- de outro.
|
|
139
|
+
'apps', coalesce((
|
|
140
|
+
SELECT jsonb_agg(x ORDER BY x ->> 'house',
|
|
141
|
+
(x ->> 'trade')::boolean DESC,
|
|
142
|
+
(x -> 'preset' -> 'rail' IS NOT NULL) DESC,
|
|
143
|
+
x ->> 'name') FROM (
|
|
144
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
145
|
+
'url', a.url, 'house', false, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
|
|
146
|
+
'trade', a.vertical_id IS NOT NULL AND a.vertical_id NOT IN ('accounting','generic')) AS x
|
|
147
|
+
FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
|
|
148
|
+
WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
|
|
149
|
+
UNION ALL
|
|
150
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
151
|
+
'url', a.url, 'house', true, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
|
|
152
|
+
'trade', false) AS x
|
|
153
|
+
FROM app.apps a WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL) apps), '[]'::jsonb),
|
|
154
|
+
'nav', coalesce((
|
|
155
|
+
SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
|
|
156
|
+
'section', n.section, 'position', n.position) ORDER BY n.entry_key)
|
|
157
|
+
FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb),
|
|
158
|
+
'permissions', coalesce((
|
|
159
|
+
SELECT jsonb_agg(DISTINCT rp.permission)
|
|
160
|
+
FROM app.memberships m
|
|
161
|
+
JOIN app.role_permissions rp ON rp.role_id = m.role_id AND rp.tenant_id = m.tenant_id
|
|
162
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = (SELECT auth.uid()) AND m.active
|
|
163
|
+
), '[]'::jsonb),
|
|
164
|
+
-- 202: what each unit switched off. Only the 'disabled' rows exist — a unit
|
|
165
|
+
-- with no row inherits the account — so the shell gets the whole map in
|
|
166
|
+
-- this same read and never asks per page.
|
|
167
|
+
'unit_plugins', coalesce((
|
|
168
|
+
SELECT jsonb_agg(jsonb_build_object('unit_id', u.unit_id, 'plugin', u.plugin_id,
|
|
169
|
+
'facet', nullif(u.facet, ''), 'status', u.status) ORDER BY u.unit_id, u.plugin_id, u.facet)
|
|
170
|
+
FROM app.unit_plugins u WHERE u.tenant_id = v_tenant), '[]'::jsonb),
|
|
171
|
+
'generated_at', now());
|
|
172
|
+
END $function$;
|
|
173
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- 203: delivery (own fleet + iFood and other marketplaces) becomes the orders
|
|
2
|
+
-- facet `delivery`, turned on in Módulos and priced on its own. Two additive,
|
|
3
|
+
-- idempotent steps so nobody loses what they use today:
|
|
4
|
+
-- 1. every plan that grants orders also permits the facet (optional);
|
|
5
|
+
-- 2. every account with orders on gets the facet on — the delivery settings
|
|
6
|
+
-- were always there for them, and a new facet defaults to off.
|
|
7
|
+
|
|
8
|
+
DO $$
|
|
9
|
+
DECLARE
|
|
10
|
+
v record;
|
|
11
|
+
BEGIN
|
|
12
|
+
IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NOT NULL THEN
|
|
13
|
+
FOR v IN
|
|
14
|
+
SELECT g.plan_id FROM app.plan_grants g
|
|
15
|
+
WHERE g.plugin_id = 'orders' AND g.facet = ''
|
|
16
|
+
AND NOT EXISTS (SELECT 1 FROM app.plan_grants x
|
|
17
|
+
WHERE x.plan_id = g.plan_id AND x.plugin_id = 'orders' AND x.facet = 'delivery')
|
|
18
|
+
LOOP
|
|
19
|
+
PERFORM public.register_plan_grant(v.plan_id, 'orders', 'delivery', 'optional',
|
|
20
|
+
'Was always on with orders; now a module the account turns on in Módulos.');
|
|
21
|
+
END LOOP;
|
|
22
|
+
END IF;
|
|
23
|
+
|
|
24
|
+
IF to_regclass('app.tenant_plugins') IS NOT NULL THEN
|
|
25
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, config, source)
|
|
26
|
+
SELECT tp.tenant_id, 'orders', 'delivery', 'active', '{}'::jsonb, 'default'
|
|
27
|
+
FROM app.tenant_plugins tp
|
|
28
|
+
WHERE tp.plugin_id = 'orders' AND tp.facet = '' AND tp.status = 'active'
|
|
29
|
+
ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
|
|
30
|
+
END IF;
|
|
31
|
+
END $$;
|