@rebasepro/server-postgresql 0.0.1-canary.dbf160a → 0.0.1-canary.e17585f
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 +683 -1362
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +614 -1293
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +8 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- 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/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/auth_adapter.d.ts +354 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +75 -11
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/database_adapter.d.ts +90 -0
- package/dist/types/src/types/entity_views.d.ts +6 -7
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +72 -88
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +12 -0
- package/package.json +5 -5
- package/src/PostgresAdapter.ts +52 -0
- package/src/PostgresBackendDriver.ts +49 -7
- package/src/PostgresBootstrapper.ts +4 -7
- package/src/auth/ensure-tables.ts +3 -121
- package/src/cli.ts +10 -2
- package/src/data-transformer.ts +84 -1
- package/src/index.ts +1 -0
- package/src/schema/doctor.ts +14 -2
- package/src/schema/generate-drizzle-schema-logic.ts +59 -30
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +365 -61
- package/src/schema/introspect-db.ts +66 -23
- package/src/services/EntityFetchService.ts +16 -0
- package/src/services/EntityPersistService.ts +95 -13
- package/src/services/realtimeService.ts +35 -0
- package/src/utils/drizzle-conditions.ts +6 -0
- package/src/websocket.ts +60 -11
- package/test/generate-drizzle-schema.test.ts +342 -0
- package/test/introspect-db-generation.test.ts +32 -10
- package/test/property-ordering.test.ts +395 -0
- package/test/relations.test.ts +4 -4
- package/jest-all.log +0 -3128
- package/jest.log +0 -49
- package/scratch.ts +0 -41
- package/test-drizzle-bug.ts +0 -18
- package/test-drizzle-out/0000_cultured_freak.sql +0 -7
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
- package/test-drizzle-out/meta/0000_snapshot.json +0 -55
- package/test-drizzle-out/meta/0001_snapshot.json +0 -63
- package/test-drizzle-out/meta/_journal.json +0 -20
- package/test-drizzle-prompt.sh +0 -2
- package/test-policy-prompt.sh +0 -3
- package/test-programmatic.ts +0 -30
- package/test-programmatic2.ts +0 -59
- package/test-schema-no-policies.ts +0 -12
- package/test_drizzle_mock.js +0 -3
- package/test_find_changed.mjs +0 -32
- package/test_hash.js +0 -14
- package/test_output.txt +0 -3145
|
@@ -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,298 @@ 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
|
+
|
|
1203
|
+
describe("generateDrizzleSchema autoValue date properties", () => {
|
|
1204
|
+
|
|
1205
|
+
it("should add .default(sql`now()`) for on_create autoValue", async () => {
|
|
1206
|
+
const collections: EntityCollection[] = [
|
|
1207
|
+
{
|
|
1208
|
+
slug: "articles",
|
|
1209
|
+
table: "articles",
|
|
1210
|
+
name: "Articles",
|
|
1211
|
+
properties: {
|
|
1212
|
+
title: { type: "string" },
|
|
1213
|
+
created_at: {
|
|
1214
|
+
type: "date",
|
|
1215
|
+
autoValue: "on_create"
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
const result = await generateSchema(collections, true);
|
|
1222
|
+
|
|
1223
|
+
// on_create should produce .default(sql`now()`)
|
|
1224
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1225
|
+
// No $onUpdate or triggers — on_update logic lives in the backend driver
|
|
1226
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1227
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1228
|
+
expect(result).not.toContain("CREATE OR REPLACE FUNCTION");
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it("should add .default(sql`now()`) for on_update autoValue (INSERT default only)", async () => {
|
|
1232
|
+
const collections: EntityCollection[] = [
|
|
1233
|
+
{
|
|
1234
|
+
slug: "articles",
|
|
1235
|
+
table: "articles",
|
|
1236
|
+
name: "Articles",
|
|
1237
|
+
properties: {
|
|
1238
|
+
title: { type: "string" },
|
|
1239
|
+
updated_at: {
|
|
1240
|
+
type: "date",
|
|
1241
|
+
autoValue: "on_update"
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
];
|
|
1246
|
+
|
|
1247
|
+
const result = await generateSchema(collections, true);
|
|
1248
|
+
|
|
1249
|
+
// on_update should produce .default(sql`now()`) for initial INSERT value
|
|
1250
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1251
|
+
// No $onUpdate or triggers — update logic is handled by the backend driver
|
|
1252
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1253
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it("should not modify date columns without autoValue", async () => {
|
|
1257
|
+
const collections: EntityCollection[] = [
|
|
1258
|
+
{
|
|
1259
|
+
slug: "events",
|
|
1260
|
+
table: "events",
|
|
1261
|
+
name: "Events",
|
|
1262
|
+
properties: {
|
|
1263
|
+
name: { type: "string" },
|
|
1264
|
+
event_date: { type: "date" }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
];
|
|
1268
|
+
|
|
1269
|
+
const result = await generateSchema(collections, true);
|
|
1270
|
+
|
|
1271
|
+
// A plain date should NOT have any autoValue-related modifiers
|
|
1272
|
+
expect(result).not.toContain(".default(sql`now()`)");
|
|
1273
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it("should handle both on_create and on_update in the same collection", async () => {
|
|
1277
|
+
const collections: EntityCollection[] = [
|
|
1278
|
+
{
|
|
1279
|
+
slug: "posts",
|
|
1280
|
+
table: "posts",
|
|
1281
|
+
name: "Posts",
|
|
1282
|
+
properties: {
|
|
1283
|
+
title: { type: "string" },
|
|
1284
|
+
created_at: {
|
|
1285
|
+
type: "date",
|
|
1286
|
+
autoValue: "on_create"
|
|
1287
|
+
},
|
|
1288
|
+
updated_at: {
|
|
1289
|
+
type: "date",
|
|
1290
|
+
autoValue: "on_update"
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
];
|
|
1295
|
+
|
|
1296
|
+
const result = await generateSchema(collections, true);
|
|
1297
|
+
|
|
1298
|
+
// Both should get .default(sql`now()`) for INSERT defaults
|
|
1299
|
+
expect(result).toMatch(/created_at:.*\.default\(sql`now\(\)`\)/);
|
|
1300
|
+
expect(result).toMatch(/updated_at:.*\.default\(sql`now\(\)`\)/);
|
|
1301
|
+
// No $onUpdate or triggers
|
|
1302
|
+
expect(result).not.toContain(".$onUpdate");
|
|
1303
|
+
expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it("should handle on_create with date columnType", async () => {
|
|
1307
|
+
const collections: EntityCollection[] = [
|
|
1308
|
+
{
|
|
1309
|
+
slug: "logs",
|
|
1310
|
+
table: "logs",
|
|
1311
|
+
name: "Logs",
|
|
1312
|
+
properties: {
|
|
1313
|
+
message: { type: "string" },
|
|
1314
|
+
log_date: {
|
|
1315
|
+
type: "date",
|
|
1316
|
+
columnType: "date",
|
|
1317
|
+
autoValue: "on_create"
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
];
|
|
1322
|
+
|
|
1323
|
+
const result = await generateSchema(collections, true);
|
|
1324
|
+
|
|
1325
|
+
// Should use date() column with the default
|
|
1326
|
+
expect(result).toContain("date(\"log_date\"");
|
|
1327
|
+
expect(result).toContain(".default(sql`now()`)");
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
@@ -148,27 +148,27 @@ describe("generateCollectionFile", () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
describe("enum support", () => {
|
|
151
|
-
it("generates
|
|
151
|
+
it("generates enum for USER-DEFINED columns with matching enum", () => {
|
|
152
152
|
const enumMap = new Map([["order_status", ["pending", "shipped", "delivered"]]]);
|
|
153
153
|
const meta = makeSimpleTable("orders", [
|
|
154
154
|
mkCol("orders", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
155
155
|
mkCol("orders", "status", { data_type: "USER-DEFINED", udt_name: "order_status" }),
|
|
156
156
|
]);
|
|
157
157
|
const result = generateCollectionFile("orders", meta, [], new Set(), new Map([["orders", meta]]), enumMap);
|
|
158
|
-
expect(result).toContain('
|
|
158
|
+
expect(result).toContain('enum:');
|
|
159
159
|
expect(result).toContain('{ id: "pending", label: "Pending" }');
|
|
160
160
|
expect(result).toContain('{ id: "shipped", label: "Shipped" }');
|
|
161
161
|
expect(result).toContain('{ id: "delivered", label: "Delivered" }');
|
|
162
162
|
expect(result).toContain('type: "string"');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("does NOT add
|
|
165
|
+
it("does NOT add enum for USER-DEFINED without matching enum", () => {
|
|
166
166
|
const meta = makeSimpleTable("things", [
|
|
167
167
|
mkCol("things", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
168
168
|
mkCol("things", "geom", { data_type: "USER-DEFINED", udt_name: "geometry" }),
|
|
169
169
|
]);
|
|
170
170
|
const result = generateCollectionFile("things", meta, [], new Set(), new Map([["things", meta]]), new Map());
|
|
171
|
-
expect(result).not.toContain("
|
|
171
|
+
expect(result).not.toContain("enum");
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
it("humanizes enum value labels with underscores", () => {
|
|
@@ -264,7 +264,7 @@ describe("generateCollectionFile", () => {
|
|
|
264
264
|
]);
|
|
265
265
|
const result = generateCollectionFile("t", meta, [], new Set(), new Map([["t", meta]]), enumMap);
|
|
266
266
|
expect(result).not.toContain("storagePath");
|
|
267
|
-
expect(result).toContain("
|
|
267
|
+
expect(result).toContain("enum:");
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
|
|
@@ -285,22 +285,40 @@ describe("generateCollectionFile", () => {
|
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
describe("inverse relation (other table -> this table)", () => {
|
|
288
|
-
it("generates a one-to-many inverse relation", () => {
|
|
288
|
+
it("generates a one-to-many inverse relation in the relations array", () => {
|
|
289
289
|
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
290
290
|
const meta = makeSimpleTable("posts", [
|
|
291
291
|
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
292
292
|
]);
|
|
293
293
|
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
294
294
|
expect(result).toContain('import commentsCollection from "./comments"');
|
|
295
|
+
// Should be in the relations array, not as an inline property
|
|
296
|
+
expect(result).toContain('relations: [');
|
|
297
|
+
expect(result).toContain('relationName: "comments"');
|
|
295
298
|
expect(result).toContain('cardinality: "many"');
|
|
296
299
|
expect(result).toContain('direction: "inverse"');
|
|
297
300
|
expect(result).toContain('inverseRelationName: "post"');
|
|
298
301
|
expect(result).toContain('foreignKeyOnTarget: "post_id"');
|
|
302
|
+
// Should NOT appear as an inline property with type: "relation"
|
|
303
|
+
const propsSection = result.split('properties:')[1].split('relations:')[0];
|
|
304
|
+
expect(propsSection).not.toContain('"relation"');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("does NOT include inverse relations in propertiesOrder", () => {
|
|
308
|
+
const allFks: ForeignKeyRow[] = [mkFk("comments", "post_id", "posts")];
|
|
309
|
+
const meta = makeSimpleTable("posts", [
|
|
310
|
+
mkCol("posts", "id", { data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
311
|
+
]);
|
|
312
|
+
const result = generateCollectionFile("posts", meta, allFks, new Set(), new Map([["posts", meta]]), new Map());
|
|
313
|
+
const orderMatch = result.match(/propertiesOrder:\s*(\[[\s\S]*?\])/);
|
|
314
|
+
expect(orderMatch).toBeTruthy();
|
|
315
|
+
const orderBlock = orderMatch![1];
|
|
316
|
+
expect(orderBlock).not.toContain('"comments"');
|
|
299
317
|
});
|
|
300
318
|
});
|
|
301
319
|
|
|
302
320
|
describe("many-to-many relations", () => {
|
|
303
|
-
it("generates owning M2M with through config
|
|
321
|
+
it("generates owning M2M with through config in relations array", () => {
|
|
304
322
|
const jtFks: ForeignKeyRow[] = [
|
|
305
323
|
mkFk("articles_tags", "article_id", "articles"),
|
|
306
324
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -317,13 +335,15 @@ describe("generateCollectionFile", () => {
|
|
|
317
335
|
const joinTables = new Set(["articles_tags"]);
|
|
318
336
|
|
|
319
337
|
const result = generateCollectionFile("articles", articlesMeta, [], joinTables, tablesMap, new Map());
|
|
338
|
+
expect(result).toContain('relations: [');
|
|
339
|
+
expect(result).toContain('relationName: "tags"');
|
|
320
340
|
expect(result).toContain('direction: "owning"');
|
|
321
341
|
expect(result).toContain('table: "articles_tags"');
|
|
322
342
|
expect(result).toContain('sourceColumn: "article_id"');
|
|
323
343
|
expect(result).toContain('targetColumn: "tag_id"');
|
|
324
344
|
});
|
|
325
345
|
|
|
326
|
-
it("generates inverse M2M
|
|
346
|
+
it("generates inverse M2M in relations array", () => {
|
|
327
347
|
const jtFks: ForeignKeyRow[] = [
|
|
328
348
|
mkFk("articles_tags", "article_id", "articles"),
|
|
329
349
|
mkFk("articles_tags", "tag_id", "tags"),
|
|
@@ -340,12 +360,13 @@ describe("generateCollectionFile", () => {
|
|
|
340
360
|
const joinTables = new Set(["articles_tags"]);
|
|
341
361
|
|
|
342
362
|
const result = generateCollectionFile("tags", tagsMeta, [], joinTables, tablesMap, new Map());
|
|
363
|
+
expect(result).toContain('relations: [');
|
|
343
364
|
expect(result).toContain('direction: "inverse"');
|
|
344
365
|
});
|
|
345
366
|
});
|
|
346
367
|
|
|
347
368
|
describe("self-referencing M2M", () => {
|
|
348
|
-
it("generates self-ref M2M with _via_
|
|
369
|
+
it("generates self-ref M2M with _via_ relation name in relations array", () => {
|
|
349
370
|
const jtFks: ForeignKeyRow[] = [
|
|
350
371
|
mkFk("user_friends", "user_id", "users"),
|
|
351
372
|
mkFk("user_friends", "friend_id", "users"),
|
|
@@ -362,7 +383,8 @@ describe("generateCollectionFile", () => {
|
|
|
362
383
|
const joinTables = new Set(["user_friends"]);
|
|
363
384
|
|
|
364
385
|
const result = generateCollectionFile("users", usersMeta, [], joinTables, tablesMap, new Map());
|
|
365
|
-
expect(result).toContain(
|
|
386
|
+
expect(result).toContain('relations: [');
|
|
387
|
+
expect(result).toContain('relationName: "users_via_friend"');
|
|
366
388
|
expect(result).toContain('table: "user_friends"');
|
|
367
389
|
expect(result).toContain('sourceColumn: "user_id"');
|
|
368
390
|
expect(result).toContain('targetColumn: "friend_id"');
|