@rebasepro/server-postgresql 0.0.1-canary.6e26b67 → 0.0.1-canary.7f31c25
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.es.js +184 -186
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +184 -186
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +6 -0
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +11 -10
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +4 -6
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +71 -87
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +4 -0
- package/package.json +5 -5
- package/src/PostgresBackendDriver.ts +9 -0
- package/src/cli.ts +1 -1
- package/src/schema/doctor.ts +14 -2
- package/src/schema/generate-drizzle-schema-logic.ts +24 -30
- package/src/schema/introspect-db-logic.ts +1 -0
- package/src/services/EntityPersistService.ts +7 -1
- package/test/generate-drizzle-schema.test.ts +214 -0
- package/test/relations.test.ts +4 -4
|
@@ -666,6 +666,53 @@ using: "{is_locked} = false" }
|
|
|
666
666
|
expect(result).toContain('as: "restrictive"');
|
|
667
667
|
});
|
|
668
668
|
|
|
669
|
+
it("should enable RLS on every table even without any security rules", async () => {
|
|
670
|
+
const collections: EntityCollection[] = [{
|
|
671
|
+
slug: "public_data",
|
|
672
|
+
table: "public_data",
|
|
673
|
+
name: "Public Data",
|
|
674
|
+
properties: { title: { type: "string" } }
|
|
675
|
+
// No securityRules defined — table should still have .enableRLS()
|
|
676
|
+
}];
|
|
677
|
+
|
|
678
|
+
const result = await generateSchema(collections);
|
|
679
|
+
expect(result).toContain(".enableRLS()");
|
|
680
|
+
// No policies should be generated
|
|
681
|
+
expect(result).not.toContain("pgPolicy(");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("should enable RLS on tables that do have security rules", async () => {
|
|
685
|
+
const collections: EntityCollection[] = [{
|
|
686
|
+
slug: "secure_data",
|
|
687
|
+
table: "secure_data",
|
|
688
|
+
name: "Secure Data",
|
|
689
|
+
properties: { title: { type: "string" } },
|
|
690
|
+
securityRules: [
|
|
691
|
+
{ operation: "select", access: "public" }
|
|
692
|
+
]
|
|
693
|
+
}];
|
|
694
|
+
|
|
695
|
+
const result = await generateSchema(collections);
|
|
696
|
+
expect(result).toContain(".enableRLS()");
|
|
697
|
+
expect(result).toContain("pgPolicy(");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("should fall back to deny-all (sql`false`) when no USING clause can be generated", async () => {
|
|
701
|
+
const collections: EntityCollection[] = [{
|
|
702
|
+
slug: "deny_test",
|
|
703
|
+
table: "deny_test",
|
|
704
|
+
name: "Deny Test",
|
|
705
|
+
properties: { title: { type: "string" } },
|
|
706
|
+
securityRules: [
|
|
707
|
+
{ operation: "select" }
|
|
708
|
+
// No access, ownerField, or using — should produce sql`false`
|
|
709
|
+
]
|
|
710
|
+
}];
|
|
711
|
+
|
|
712
|
+
const result = await generateSchema(collections);
|
|
713
|
+
expect(result).toContain("sql`false`");
|
|
714
|
+
});
|
|
715
|
+
|
|
669
716
|
});
|
|
670
717
|
});
|
|
671
718
|
// V2 improvements tests
|
|
@@ -986,3 +1033,170 @@ isId: true }
|
|
|
986
1033
|
expect(cleanResult).toContain("user_name: varchar(\"user_name\").primaryKey()");
|
|
987
1034
|
});
|
|
988
1035
|
});
|
|
1036
|
+
|
|
1037
|
+
// ── columnName tests ────────────────────────────────────────────────────
|
|
1038
|
+
describe("generateDrizzleSchema columnName support", () => {
|
|
1039
|
+
const cleanSchema = (schema: string) => {
|
|
1040
|
+
return schema
|
|
1041
|
+
.replace(/\/\/.*$/gm, "")
|
|
1042
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1043
|
+
.replace(/\n{2,}/g, "\n")
|
|
1044
|
+
.replace(/\s+/g, " ")
|
|
1045
|
+
.trim();
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
it("should use explicit columnName as the SQL column name instead of deriving from the property key", async () => {
|
|
1049
|
+
const collections: EntityCollection[] = [{
|
|
1050
|
+
slug: "billing",
|
|
1051
|
+
table: "company_billing_config",
|
|
1052
|
+
name: "Billing",
|
|
1053
|
+
properties: {
|
|
1054
|
+
employee_number_140a: {
|
|
1055
|
+
type: "string",
|
|
1056
|
+
name: "Employee Number 140a",
|
|
1057
|
+
columnName: "employee_number_140a",
|
|
1058
|
+
},
|
|
1059
|
+
contract_number_140a: {
|
|
1060
|
+
type: "string",
|
|
1061
|
+
name: "Contract Number 140a",
|
|
1062
|
+
columnName: "contract_number_140a",
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
}];
|
|
1066
|
+
|
|
1067
|
+
const result = await generateSchema(collections);
|
|
1068
|
+
const cleanResult = cleanSchema(result);
|
|
1069
|
+
|
|
1070
|
+
// Must use the exact columnName, NOT toSnakeCase(propKey) which would produce "employee_number_140_a"
|
|
1071
|
+
expect(cleanResult).toContain('employee_number_140a: varchar("employee_number_140a")');
|
|
1072
|
+
expect(cleanResult).toContain('contract_number_140a: varchar("contract_number_140a")');
|
|
1073
|
+
|
|
1074
|
+
// Must NOT contain the broken snake_case version
|
|
1075
|
+
expect(cleanResult).not.toContain("employee_number_140_a");
|
|
1076
|
+
expect(cleanResult).not.toContain("contract_number_140_a");
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it("should fall back to toSnakeCase when columnName is not set (manually-authored collections)", async () => {
|
|
1080
|
+
const collections: EntityCollection[] = [{
|
|
1081
|
+
slug: "products",
|
|
1082
|
+
table: "products",
|
|
1083
|
+
name: "Products",
|
|
1084
|
+
properties: {
|
|
1085
|
+
productName: {
|
|
1086
|
+
type: "string",
|
|
1087
|
+
name: "Product Name",
|
|
1088
|
+
// No columnName — should derive from key
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
}];
|
|
1092
|
+
|
|
1093
|
+
const result = await generateSchema(collections);
|
|
1094
|
+
const cleanResult = cleanSchema(result);
|
|
1095
|
+
|
|
1096
|
+
// JS key stays camelCase, SQL column name gets snake_cased
|
|
1097
|
+
expect(cleanResult).toContain('productName: varchar("product_name")');
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("should handle mixed properties — some with columnName, some without", async () => {
|
|
1101
|
+
const collections: EntityCollection[] = [{
|
|
1102
|
+
slug: "config",
|
|
1103
|
+
table: "app_config",
|
|
1104
|
+
name: "Config",
|
|
1105
|
+
properties: {
|
|
1106
|
+
// Introspected: has explicit columnName
|
|
1107
|
+
fee_number_140a: {
|
|
1108
|
+
type: "string",
|
|
1109
|
+
name: "Fee Number",
|
|
1110
|
+
columnName: "fee_number_140a",
|
|
1111
|
+
},
|
|
1112
|
+
// Manually added: no columnName
|
|
1113
|
+
displayName: {
|
|
1114
|
+
type: "string",
|
|
1115
|
+
name: "Display Name",
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
}];
|
|
1119
|
+
|
|
1120
|
+
const result = await generateSchema(collections);
|
|
1121
|
+
const cleanResult = cleanSchema(result);
|
|
1122
|
+
|
|
1123
|
+
// Introspected prop uses exact columnName
|
|
1124
|
+
expect(cleanResult).toContain('fee_number_140a: varchar("fee_number_140a")');
|
|
1125
|
+
// Manual prop: JS key stays camelCase, SQL column gets snake_cased
|
|
1126
|
+
expect(cleanResult).toContain('displayName: varchar("display_name")');
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("should use columnName for all property types, not just strings", async () => {
|
|
1130
|
+
const collections: EntityCollection[] = [{
|
|
1131
|
+
slug: "metrics",
|
|
1132
|
+
table: "metrics",
|
|
1133
|
+
name: "Metrics",
|
|
1134
|
+
properties: {
|
|
1135
|
+
count_v2: {
|
|
1136
|
+
type: "number",
|
|
1137
|
+
name: "Count V2",
|
|
1138
|
+
columnName: "count_v2",
|
|
1139
|
+
},
|
|
1140
|
+
is_active_v2: {
|
|
1141
|
+
type: "boolean",
|
|
1142
|
+
name: "Is Active V2",
|
|
1143
|
+
columnName: "is_active_v2",
|
|
1144
|
+
},
|
|
1145
|
+
created_at_v2: {
|
|
1146
|
+
type: "date",
|
|
1147
|
+
name: "Created At V2",
|
|
1148
|
+
columnName: "created_at_v2",
|
|
1149
|
+
},
|
|
1150
|
+
metadata_v2: {
|
|
1151
|
+
type: "map",
|
|
1152
|
+
name: "Metadata V2",
|
|
1153
|
+
columnName: "metadata_v2",
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
}];
|
|
1157
|
+
|
|
1158
|
+
const result = await generateSchema(collections);
|
|
1159
|
+
const cleanResult = cleanSchema(result);
|
|
1160
|
+
|
|
1161
|
+
expect(cleanResult).toContain('count_v2: numeric("count_v2")');
|
|
1162
|
+
expect(cleanResult).toContain('is_active_v2: boolean("is_active_v2")');
|
|
1163
|
+
expect(cleanResult).toContain('created_at_v2: timestamp("created_at_v2"');
|
|
1164
|
+
expect(cleanResult).toContain('metadata_v2: jsonb("metadata_v2")');
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it("should reproduce and prevent the medmot bug: digit+letter column names", async () => {
|
|
1168
|
+
// This is the exact scenario from the medmot project that caused the production failure
|
|
1169
|
+
const collections: EntityCollection[] = [{
|
|
1170
|
+
slug: "company_billing_config",
|
|
1171
|
+
table: "company_billing_config",
|
|
1172
|
+
name: "Company Billing Config",
|
|
1173
|
+
properties: {
|
|
1174
|
+
employee_number_140a: { type: "string", name: "Employee Number", columnName: "employee_number_140a" },
|
|
1175
|
+
contract_number_140a: { type: "string", name: "Contract Number", columnName: "contract_number_140a" },
|
|
1176
|
+
amount: { type: "number", name: "Amount" },
|
|
1177
|
+
id: { type: "number", name: "ID", isId: "increment" },
|
|
1178
|
+
service_provider_140a: { type: "string", name: "Service Provider", columnName: "service_provider_140a" },
|
|
1179
|
+
internal_area_code_140a: { type: "string", name: "Internal Area Code", columnName: "internal_area_code_140a" },
|
|
1180
|
+
fee_number_140a: { type: "string", name: "Fee Number", columnName: "fee_number_140a" },
|
|
1181
|
+
receiver_market_participant_140a: { type: "string", name: "Receiver Market Participant", columnName: "receiver_market_participant_140a" },
|
|
1182
|
+
employee_value_number_140a: { type: "string", name: "Employee Value Number", columnName: "employee_value_number_140a" },
|
|
1183
|
+
sender_market_participant_140a: { type: "string", name: "Sender Market Participant", columnName: "sender_market_participant_140a" },
|
|
1184
|
+
processing_indicator_140a: { type: "string", name: "Processing Indicator", columnName: "processing_indicator_140a" },
|
|
1185
|
+
insurance_id_140a: { type: "string", name: "Insurance ID", columnName: "insurance_id_140a" },
|
|
1186
|
+
company_id: { type: "number", name: "Company ID" },
|
|
1187
|
+
},
|
|
1188
|
+
}];
|
|
1189
|
+
|
|
1190
|
+
const result = await generateSchema(collections);
|
|
1191
|
+
|
|
1192
|
+
// Every _140a column must stay _140a, not become _140_a
|
|
1193
|
+
const brokenPattern = /_140_a/;
|
|
1194
|
+
expect(result).not.toMatch(brokenPattern);
|
|
1195
|
+
|
|
1196
|
+
// Spot-check a few exact columns
|
|
1197
|
+
expect(result).toContain('"employee_number_140a"');
|
|
1198
|
+
expect(result).toContain('"contract_number_140a"');
|
|
1199
|
+
expect(result).toContain('"service_provider_140a"');
|
|
1200
|
+
expect(result).toContain('"insurance_id_140a"');
|
|
1201
|
+
});
|
|
1202
|
+
});
|
package/test/relations.test.ts
CHANGED
|
@@ -382,8 +382,8 @@ relationName: "author" }
|
|
|
382
382
|
// Should create owning relation on profiles
|
|
383
383
|
expect(cleanResult).toContain("export const profilesRelations = drizzleRelations(profiles, ({ one, many }) => ({ \"author\": one(authors, { fields: [profiles.author_id], references: [authors.id], relationName: \"profiles_author_id\" }) }));");
|
|
384
384
|
|
|
385
|
-
// Should create inverse relation on authors
|
|
386
|
-
expect(cleanResult).toContain("export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ \"profile\": one(profiles, {
|
|
385
|
+
// Should create inverse relation on authors — inverse side has NO fields/references
|
|
386
|
+
expect(cleanResult).toContain("export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ \"profile\": one(profiles, { relationName: \"profiles_author_id\" }) }));");
|
|
387
387
|
});
|
|
388
388
|
|
|
389
389
|
it("should generate owning one-to-many relations", async () => {
|
|
@@ -818,9 +818,9 @@ relationName: "user" }
|
|
|
818
818
|
`"user": one(users, { fields: [profiles.user_id], references: [users.id], relationName: \"${expectedSharedName}\" })`
|
|
819
819
|
);
|
|
820
820
|
|
|
821
|
-
// Inverse side (users → profiles)
|
|
821
|
+
// Inverse side (users → profiles) — no fields/references, paired by relationName only
|
|
822
822
|
expect(cleanResult).toContain(
|
|
823
|
-
`"profile": one(profiles, {
|
|
823
|
+
`"profile": one(profiles, { relationName: \"${expectedSharedName}\" })`
|
|
824
824
|
);
|
|
825
825
|
|
|
826
826
|
// Both must match
|