Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ console.log(user);
// }
```

*Note: many-to-many relationships are only supported with explicit foreign keys.*
*Note: see an example of many-to-many relationships with an explicit mapping of the junction/target columns in [many-to-many-extended-config.zero.ts](tests/schemas/many-to-many-extended-config.zero.ts).*

## Features

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-zero",
"version": "0.2.2",
"version": "0.2.3",
"description": "Generate Zero schemas from Drizzle ORM schemas",
"type": "module",
"scripts": {
Expand Down
238 changes: 159 additions & 79 deletions src/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ type ColumnIndexKeys<TTable extends Table> = Readonly<
}[keyof Columns<TTable>]
>;

/**
* Extracts the table name from a configuration object or string.
* @template TTableConfig - The configuration object or string
*/
type ExtractTableConfigName<TTableConfig> = TTableConfig extends {
readonly destTable: string;
}
? TTableConfig["destTable"]
: TTableConfig extends string
? TTableConfig
: never;

/**
* Represents the structure of a many-to-many relationship through a junction table.
* @template TDrizzleSchema - The complete Drizzle schema
Expand Down Expand Up @@ -107,26 +119,26 @@ type ManyToManyRelationship<
ColumnIndexKeys<
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][0],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][0]
>
>
>
>;
readonly destSchema: () => ZeroSchemaWithRelations<
FindTableByName<
TDrizzleSchema,
Extract<TManyConfig[TableName<TCurrentTable>][K][0], string>
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][0]
>
>,
ResolveColumnConfig<
TDrizzleSchema,
TColumnConfig,
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][0],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][0]
>
>
>,
Expand All @@ -136,9 +148,8 @@ type ManyToManyRelationship<
TManyConfig,
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][0],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][0]
>
>
>
Expand All @@ -149,9 +160,8 @@ type ManyToManyRelationship<
ColumnIndexKeys<
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][0],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][0]
>
>
>
Expand All @@ -160,26 +170,26 @@ type ManyToManyRelationship<
ColumnIndexKeys<
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][1],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][1]
>
>
>
>;
readonly destSchema: () => ZeroSchemaWithRelations<
FindTableByName<
TDrizzleSchema,
Extract<TManyConfig[TableName<TCurrentTable>][K][1], string>
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][1]
>
>,
ResolveColumnConfig<
TDrizzleSchema,
TColumnConfig,
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][1],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][1]
>
>
>,
Expand All @@ -189,9 +199,8 @@ type ManyToManyRelationship<
TManyConfig,
FindTableByName<
TDrizzleSchema,
Extract<
TManyConfig[TableName<TCurrentTable>][K][1],
string
ExtractTableConfigName<
TManyConfig[TableName<TCurrentTable>][K][1]
>
>
>
Expand Down Expand Up @@ -261,8 +270,8 @@ type DirectRelationships<
TManyConfig extends ManyConfig<TDrizzleSchema, TColumnConfig>,
TCurrentTable extends Table,
TCurrentTableRelations extends Relations,
> = TCurrentTableRelations extends never
? never
> = [TCurrentTableRelations] extends [never]
? {}
: {
readonly [K in ValidDirectRelationKeys<
TDrizzleSchema,
Expand Down Expand Up @@ -315,12 +324,29 @@ type ManyTableConfig<
TColumnConfig extends TableColumnsConfig<TDrizzleSchema>,
TSourceTableName extends keyof TColumnConfig,
> = {
readonly [TRelationName: string]: {
[K in Exclude<keyof TColumnConfig, TSourceTableName>]: [
K,
Exclude<keyof TColumnConfig, TSourceTableName | K>,
];
}[Exclude<keyof TColumnConfig, TSourceTableName>];
readonly [TRelationName: string]:
| {
[K in Exclude<keyof TColumnConfig, TSourceTableName>]: readonly [
K,
Exclude<keyof TColumnConfig, TSourceTableName | K>,
];
}[Exclude<keyof TColumnConfig, TSourceTableName>]
| {
[K in Exclude<keyof TColumnConfig, TSourceTableName>]: {
[L in Exclude<keyof TColumnConfig, K>]: readonly [
{
readonly sourceField: keyof TColumnConfig[TSourceTableName];
readonly destTable: K;
readonly destField: keyof TColumnConfig[K];
},
{
readonly sourceField: keyof TColumnConfig[K];
readonly destTable: L;
readonly destField: keyof TColumnConfig[L];
},
];
}[Exclude<keyof TColumnConfig, K>];
}[Exclude<keyof TColumnConfig, TSourceTableName>];
};

/**
Expand Down Expand Up @@ -497,60 +523,114 @@ const createZeroSchema = <

for (const [
relationName,
[junctionTableName, destTableName],
[junctionTableNameOrObject, destTableNameOrObject],
] of Object.entries(manyConfig)) {
const sourceTable = Object.values(schema).find(
(t): t is Table =>
is(t, Table) && getTableName(t) === sourceTableName,
);
const destTable = Object.values(schema).find(
(t): t is Table => is(t, Table) && getTableName(t) === destTableName,
);

if (!sourceTable || !destTable) {
throw new Error(
`drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find ${!sourceTable ? "source" : !destTable ? "destination" : "junction"} table`,
);
}
if (
typeof junctionTableNameOrObject === "string" &&
typeof destTableNameOrObject === "string"
) {
const junctionTableName = junctionTableNameOrObject;

// Find source->junction and junction->dest relationships
const sourceJunctionFields = findForeignKeySourceAndDestFields(schema, {
sourceTable: sourceTable,
referencedTableName: junctionTableName,
});
const destTableName = destTableNameOrObject;

const junctionDestFields = findForeignKeySourceAndDestFields(schema, {
sourceTable: destTable,
referencedTableName: junctionTableName,
});
const sourceTable = Object.values(schema).find(
(t): t is Table =>
is(t, Table) && getTableName(t) === sourceTableName,
);
const destTable = Object.values(schema).find(
(t): t is Table =>
is(t, Table) && getTableName(t) === destTableName,
);

if (
!sourceJunctionFields.sourceFieldNames.length ||
!junctionDestFields.sourceFieldNames.length ||
!junctionDestFields.destFieldNames.length ||
!sourceJunctionFields.destFieldNames.length
) {
throw new Error(
`drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find foreign key relationships in junction table ${junctionTableName}`,
if (!sourceTable || !destTable) {
throw new Error(
`drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find ${!sourceTable ? "source" : !destTable ? "destination" : "junction"} table`,
);
}

// Find source->junction and junction->dest relationships
const sourceJunctionFields = findForeignKeySourceAndDestFields(
schema,
{
sourceTable: sourceTable,
referencedTableName: junctionTableName,
},
);
}

(
relationships[
sourceTableName as keyof typeof relationships
] as Record<string, unknown>
)[relationName] = [
{
sourceField: sourceJunctionFields.sourceFieldNames,
destField: sourceJunctionFields.destFieldNames,
destSchemaTableName: junctionTableName,
},
{
sourceField: junctionDestFields.destFieldNames,
destField: junctionDestFields.sourceFieldNames,
destSchemaTableName: destTableName,
},
];
const junctionDestFields = findForeignKeySourceAndDestFields(schema, {
sourceTable: destTable,
referencedTableName: junctionTableName,
});

if (
!sourceJunctionFields.sourceFieldNames.length ||
!junctionDestFields.sourceFieldNames.length ||
!junctionDestFields.destFieldNames.length ||
!sourceJunctionFields.destFieldNames.length
) {
throw new Error(
`drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Could not find foreign key relationships in junction table ${junctionTableName}`,
);
}

(
relationships[
sourceTableName as keyof typeof relationships
] as Record<string, unknown>
)[relationName] = [
{
sourceField: sourceJunctionFields.sourceFieldNames,
destField: sourceJunctionFields.destFieldNames,
destSchemaTableName: junctionTableName,
},
{
sourceField: junctionDestFields.destFieldNames,
destField: junctionDestFields.sourceFieldNames,
destSchemaTableName: destTableName,
},
];
} else {
const junctionTableName =
junctionTableNameOrObject?.destTable ?? null;
const junctionSourceField =
junctionTableNameOrObject?.sourceField ?? null;
const junctionDestField =
junctionTableNameOrObject?.destField ?? null;

const destTableName = destTableNameOrObject?.destTable ?? null;
const destSourceField = destTableNameOrObject?.sourceField ?? null;
const destDestField = destTableNameOrObject?.destField ?? null;

if (
!junctionSourceField ||
!junctionDestField ||
!destSourceField ||
!destDestField ||
!junctionTableName ||
!destTableName
) {
throw new Error(
`drizzle-zero: Invalid many-to-many configuration for ${String(sourceTableName)}.${relationName}: Not all required fields were provided.`,
);
}

(
relationships[
sourceTableName as keyof typeof relationships
] as Record<string, unknown>
)[relationName] = [
{
sourceField: [junctionSourceField],
destField: [junctionDestField],
destSchemaTableName: junctionTableName,
},
{
sourceField: [destSourceField],
destField: [destDestField],
destSchemaTableName: destTableName,
},
];
}
}
}
}
Expand Down
29 changes: 22 additions & 7 deletions tests/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,10 @@ describe.concurrent("compile", () => {
test("compile - no-relations", async () => {
const result = await runZeroBuildSchema("no-relations");
expect(result.schema.tables.user).toBeTruthy();
expect(Object.keys(result.schema.tables)).toStrictEqual(["profile_info", "user"]);
});

test("compile - one-to-one", async () => {
const result = await runZeroBuildSchema("one-to-one");
expect(result.schema.tables.user).toBeTruthy();
expect(Object.keys(result.schema.tables)).toStrictEqual(["profile_info", "user"]);
expect(Object.keys(result.schema.tables)).toStrictEqual([
"profile_info",
"user",
]);
});

test("compile - one-to-one-2", async () => {
Expand All @@ -59,6 +56,15 @@ describe.concurrent("compile", () => {
]);
});

test("compile - one-to-one", async () => {
const result = await runZeroBuildSchema("one-to-one");
expect(result.schema.tables.user).toBeTruthy();
expect(Object.keys(result.schema.tables)).toStrictEqual([
"profile_info",
"user",
]);
});

test("compile - one-to-one-subset", async () => {
const result = await runZeroBuildSchema("one-to-one-subset");
expect(result.schema.tables.user).toBeTruthy();
Expand Down Expand Up @@ -118,6 +124,15 @@ describe.concurrent("compile", () => {
]);
});

test("compile - many-to-many-self-referential", async () => {
const result = await runZeroBuildSchema("many-to-many-self-referential");
expect(result.schema.tables.user).toBeTruthy();
expect(Object.keys(result.schema.tables)).toStrictEqual([
"friendship",
"user",
]);
});

test("compile - custom-schema", async () => {
const result = await runZeroBuildSchema("custom-schema");
expect(result.schema.tables.user).toBeTruthy();
Expand Down
Loading
Loading