@fayz-ai/db 0.24.0 → 0.24.2
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/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/migrations/209_the_store_gets_its_rail_group.sql +37 -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,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 $$;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- 204: a module switched off in a unit (202) stops WRITING there, not only
|
|
2
|
+
-- showing. One guard, a BEFORE INSERT/UPDATE trigger on each module's
|
|
3
|
+
-- operational tables: a sale, a tab, a booking, a stock move, a time entry in
|
|
4
|
+
-- a unit that has the module off is refused.
|
|
5
|
+
--
|
|
6
|
+
-- Who it binds: a signed-in person (auth.role() = 'authenticated'), including
|
|
7
|
+
-- through SECURITY DEFINER RPCs — those still carry the caller's JWT, so the
|
|
8
|
+
-- sale RPCs are covered without touching them. service_role and direct SQL
|
|
9
|
+
-- (no JWT: migrations, ETL, support) pass, as every other guard here does.
|
|
10
|
+
-- Catalog/config tables are left alone (a price list is shared by the network),
|
|
11
|
+
-- and so are rows one module writes on another's behalf — the stock move, the
|
|
12
|
+
-- fiscal document and the cash line a sale produces: the sale is the act.
|
|
13
|
+
-- Additive and idempotent.
|
|
14
|
+
|
|
15
|
+
-- Only the UNIT switch (202). The account-level status is the runtime's call:
|
|
16
|
+
-- a module the account turned off can still run because another needs it
|
|
17
|
+
-- (orders keeps inventory moving), and refusing those writes would fail a sale.
|
|
18
|
+
CREATE OR REPLACE FUNCTION app.module_writes_in_unit(p_tenant uuid, p_unit uuid, p_plugin text, p_facet text DEFAULT '')
|
|
19
|
+
RETURNS boolean
|
|
20
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
21
|
+
SET search_path TO ''
|
|
22
|
+
AS $$
|
|
23
|
+
SELECT NOT EXISTS (
|
|
24
|
+
SELECT 1 FROM app.unit_plugins u
|
|
25
|
+
WHERE u.tenant_id = p_tenant AND u.unit_id = p_unit AND u.plugin_id = p_plugin
|
|
26
|
+
AND u.facet IN ('', coalesce(p_facet, '')) AND u.status = 'disabled')
|
|
27
|
+
$$;
|
|
28
|
+
REVOKE ALL ON FUNCTION app.module_writes_in_unit(uuid, uuid, text, text) FROM PUBLIC;
|
|
29
|
+
GRANT EXECUTE ON FUNCTION app.module_writes_in_unit(uuid, uuid, text, text) TO authenticated, service_role;
|
|
30
|
+
|
|
31
|
+
-- TG_ARGV[0] = plugin, TG_ARGV[1] = facet (optional).
|
|
32
|
+
CREATE OR REPLACE FUNCTION app.trg_module_unit_guard()
|
|
33
|
+
RETURNS trigger
|
|
34
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
35
|
+
SET search_path TO ''
|
|
36
|
+
AS $$
|
|
37
|
+
DECLARE
|
|
38
|
+
v_plugin text := TG_ARGV[0];
|
|
39
|
+
v_facet text := coalesce(TG_ARGV[1], '');
|
|
40
|
+
BEGIN
|
|
41
|
+
IF coalesce(auth.role(), '') <> 'authenticated' OR NEW.unit_id IS NULL THEN
|
|
42
|
+
RETURN NEW;
|
|
43
|
+
END IF;
|
|
44
|
+
-- Sales: the channel says which module sold it.
|
|
45
|
+
IF TG_TABLE_NAME = 'orders' AND TG_TABLE_SCHEMA = 'public' THEN
|
|
46
|
+
IF NEW.channel = 'shop' THEN v_plugin := 'shop';
|
|
47
|
+
ELSIF NEW.channel = 'delivery' THEN v_plugin := 'orders'; v_facet := 'delivery';
|
|
48
|
+
ELSIF NEW.channel IS NULL OR NEW.channel NOT IN ('balcao', 'dine_in', 'retirada') THEN RETURN NEW;
|
|
49
|
+
END IF;
|
|
50
|
+
END IF;
|
|
51
|
+
IF NOT app.module_writes_in_unit(NEW.tenant_id, NEW.unit_id, v_plugin, v_facet) THEN
|
|
52
|
+
RAISE EXCEPTION 'module % is switched off in this unit', v_plugin || CASE WHEN v_facet = '' THEN '' ELSE '.' || v_facet END
|
|
53
|
+
USING ERRCODE = '42501',
|
|
54
|
+
HINT = 'Ligue o módulo para esta unidade em Configurações › Módulos.';
|
|
55
|
+
END IF;
|
|
56
|
+
RETURN NEW;
|
|
57
|
+
END $$;
|
|
58
|
+
REVOKE ALL ON FUNCTION app.trg_module_unit_guard() FROM PUBLIC;
|
|
59
|
+
|
|
60
|
+
DO $$
|
|
61
|
+
DECLARE
|
|
62
|
+
r record;
|
|
63
|
+
BEGIN
|
|
64
|
+
FOR r IN SELECT * FROM (VALUES
|
|
65
|
+
('public', 'orders', 'orders', ''),
|
|
66
|
+
('public', 'plg_tables_order_guests', 'tables', ''),
|
|
67
|
+
('public', 'plg_tables_order_payments', 'tables', ''),
|
|
68
|
+
('public', 'plg_tables_order_transfers', 'tables', ''),
|
|
69
|
+
('public', 'plg_tables_order_cancellations','tables', ''),
|
|
70
|
+
('public', 'plg_tables_order_extensions', 'tables', ''),
|
|
71
|
+
('public', 'plg_tables_order_cover_charges','tables', ''),
|
|
72
|
+
('public', 'plg_tables_courtesy_events', 'tables', ''),
|
|
73
|
+
('public', 'plg_tables_waiter_calls', 'tables', ''),
|
|
74
|
+
('public', 'plg_tables_waitlist', 'tables', 'waitlist'),
|
|
75
|
+
('public', 'plg_reservations', 'reservations',''),
|
|
76
|
+
('public', 'appointments', 'agenda', ''),
|
|
77
|
+
('public', 'appointment_items', 'agenda', ''),
|
|
78
|
+
('public', 'plg_agenda_turn_entries', 'agenda', ''),
|
|
79
|
+
('public', 'plg_clinical_sessions', 'clinical', ''),
|
|
80
|
+
('public', 'plg_clinical_session_products','clinical', ''),
|
|
81
|
+
('public', 'plg_inventory_count_sessions', 'inventory', ''),
|
|
82
|
+
('public', 'plg_inventory_production_orders','production',''),
|
|
83
|
+
('public', 'plg_workforce_time_entries', 'workforce', 'time_clock'),
|
|
84
|
+
('public', 'plg_workforce_time_adjustments','workforce', ''),
|
|
85
|
+
('public', 'plg_workforce_schedule_assignments','workforce','')
|
|
86
|
+
) AS t(sch, tbl, plugin, facet)
|
|
87
|
+
LOOP
|
|
88
|
+
CONTINUE WHEN to_regclass(format('%I.%I', r.sch, r.tbl)) IS NULL;
|
|
89
|
+
CONTINUE WHEN NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
90
|
+
WHERE table_schema = r.sch AND table_name = r.tbl AND column_name = 'unit_id');
|
|
91
|
+
EXECUTE format('DROP TRIGGER IF EXISTS module_unit_guard ON %I.%I', r.sch, r.tbl);
|
|
92
|
+
EXECUTE format('CREATE TRIGGER module_unit_guard BEFORE INSERT OR UPDATE ON %I.%I
|
|
93
|
+
FOR EACH ROW EXECUTE FUNCTION app.trg_module_unit_guard(%L, %L)', r.sch, r.tbl, r.plugin, r.facet);
|
|
94
|
+
END LOOP;
|
|
95
|
+
END $$;
|