Skip to content

Commit

Permalink
feat: model and enum export path annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertoNegro committed Aug 24, 2022
1 parent bd116b7 commit b818093
Show file tree
Hide file tree
Showing 29 changed files with 1,377 additions and 313 deletions.
231 changes: 137 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ Prisma is a Node.JS ORM and includes Prisma Migrate, which uses Prisma schema (*
Obviously, Laravel is not a Node.JS environment, and Laravel already provides us a very good ORM, Eloquent. But we still can use Prisma Migrate to easily manage our database.

And via this generator, we can also generate automatically your Eloquent models, filled with PHPDocs, attributes, casts, validation rules, ..., directly from your Prisma schema.

<table>
<thead>
<tr>
<th>From</th>
<th>To</th>
</tr>
</thead>
<tr>
<td>

From a simple Prisma schema:
```prisma
/// trait:App\Traits\TokenOwner
model User {
Expand All @@ -35,10 +44,11 @@ model User {
updated_at DateTime? /// read-only
deleted_at DateTime? /// read-only
}
```

To a migrated database and a Laravel model ready to be used:
</td>
<td>

```php
<?php

Expand All @@ -55,97 +65,101 @@ use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule;

/**
* PrismaUser Model
*
* @mixin Builder
*
* @method static Builder|static query()
* @method static static make(array $attributes = [])
* @method static static create(array $attributes = [])
* @method static static forceCreate(array $attributes)
* @method static firstOrNew(array $attributes = [], array $values = [])
* @method static firstOrFail($columns = ['*'])
* @method static firstOrCreate(array $attributes, array $values = [])
* @method static firstOr($columns = ['*'], Closure $callback = null)
* @method static firstWhere($column, $operator = null, $value = null, $boolean = 'and')
* @method static updateOrCreate(array $attributes, array $values = [])
* @method null|static first($columns = ['*'])
* @method static static findOrFail($id, $columns = ['*'])
* @method static static findOrNew($id, $columns = ['*'])
* @method static null|static find($id, $columns = ['*'])
*
* @property int $id
* @property string $name
* @property-read string $email
* @property ?Carbon $email_verified_at
* @property string $password
* @property ?string $remember_token
* @property int $category_id
* @property-read ?CarbonImmutable $created_at
* @property-read ?CarbonImmutable $updated_at
* @property-read ?CarbonImmutable $deleted_at
*

* @property-read UserCategory $category
*/
* PrismaUser Model
*
* @mixin Builder
*
* @method static Builder|static query()
* @method static static make(array $attributes = [])
* @method static static create(array $attributes = [])
* @method static static forceCreate(array $attributes)
* @method static firstOrNew(array $attributes = [], array $values = [])
* @method static firstOrFail($columns = ['*'])
* @method static firstOrCreate(array $attributes, array $values = [])
* @method static firstOr($columns = ['*'], Closure $callback = null)
* @method static firstWhere($column, $operator = null, $value = null, $boolean = 'and')
* @method static updateOrCreate(array $attributes, array $values = [])
* @method null|static first($columns = ['*'])
* @method static static findOrFail($id, $columns = ['*'])
* @method static static findOrNew($id, $columns = ['*'])
* @method static null|static find($id, $columns = ['*'])
*
* @property int $id
* @property string $name
* @property-read string $email
* @property ?Carbon $email_verified_at
* @property string $password
* @property ?string $remember_token
* @property int $category_id
* @property-read ?CarbonImmutable $created_at
* @property-read ?CarbonImmutable $updated_at
* @property-read ?CarbonImmutable $deleted_at
*
* @property-read UserCategory $category
*/
abstract class PrismaUser extends Model
{
use TokenOwner;
use SoftDeletes;

protected $table = 'User';

protected $guarded = ['remember_token'];

protected $hidden = ['email_verified_at', 'password', 'remember_token'];

protected array $rules;

protected $casts = [
'id' => 'integer',
'name' => 'string',
'email' => 'string',
'email_verified_at' => 'immutable_datetime',
'password' => 'string',
'remember_token' => 'string',
'category_id' => 'integer',
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
'deleted_at' => 'immutable_datetime',
];

public function __construct(array $attributes = [])
{
$this->rules = [
'id' => ['required', 'numeric', 'integer'],
'name' => ['required', 'string'],
'email' => [
Rule::unique('User', 'email')->ignore(
$this->getKey(),
$this->getKeyName()
),
'required',
'string',
],
'email_verified_at' => ['nullable', 'date'],
'password' => ['required', 'string'],
'remember_token' => ['nullable', 'string'],
'category_id' => ['required', 'numeric', 'integer'],
'created_at' => ['nullable', 'date'],
'updated_at' => ['nullable', 'date'],
'deleted_at' => ['nullable', 'date'],
...$this->rules ?? [],
];

parent::__construct($attributes);
}

public function category()
{
return $this->belongsTo(UserCategory::class, 'category_id', 'id');
}
}
```
use TokenOwner;
use SoftDeletes;

protected $table = 'User';

protected $guarded = ['remember_token'];

protected $hidden = ['email_verified_at', 'password', 'remember_token'];

protected array $rules;

protected $casts = [
'id' => 'integer',
'name' => 'string',
'email' => 'string',
'email_verified_at' => 'immutable_datetime',
'password' => 'string',
'remember_token' => 'string',
'category_id' => 'integer',
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
'deleted_at' => 'immutable_datetime',
];

public function __construct(array $attributes = [])
{
$this->rules = [
'id' => ['required', 'numeric', 'integer'],
'name' => ['required', 'string'],
'email' => [
Rule::unique('User', 'email')->ignore(
$this->getKey(),
$this->getKeyName()
),
'required',
'string',
],
'email_verified_at' => ['nullable', 'date'],
'password' => ['required', 'string'],
'remember_token' => ['nullable', 'string'],
'category_id' => ['required', 'numeric', 'integer'],
'created_at' => ['nullable', 'date'],
'updated_at' => ['nullable', 'date'],
'deleted_at' => ['nullable', 'date'],
...$this->rules ?? [],
];

parent::__construct($attributes);
}

public function category()
{
return $this->belongsTo(UserCategory::class, 'category_id', 'id');
}
}
```

</td>
</tr>
</table>


# Initial configuration

Expand Down Expand Up @@ -592,6 +606,16 @@ model Example {
}
```

#### Path: `path`
If you need to export a specific model to a different folder to the one defined in your generator settings as `modelsPath` (see [generator settings](#generator-settings)),you can use this annotation. The path is relative to the `output` path defined in your generator settings.

```prisma
/// path:./app/Models/domain
model Example {
id Int @id @default(autoincrement())
}
```

### Field annotations

There are several annotations available for fields. In this list, we're also covering some of the most important Prisma attributes (the ones that start with `@`). To better understand how to manage fields in Prisma and to check all the available attributes in Prisma, look at the [official documentation](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#model-fields).
Expand Down Expand Up @@ -648,12 +672,14 @@ model Example {
```

#### Timestamps: `created_at` and `updated_at`
By default, if your field's name is `created_at` or `updated_at`, you don't need to add these annotations.
By default, if your field's name is `created_at` or `updated_at`, you don't need to add these annotations since they're automatically classified as (created/updated)-at timestamp and considered read-only.

If you define a `created_at`, you have to define also an `updated_at` field (and vice-versa).

You might use `@updatedAt` instead of `/// updated_at`, if you prefer.

Fields with these annotations are automatically considered as read-only.

```prisma
model Example {
id Int @id @default(autoincrement())
Expand All @@ -663,10 +689,12 @@ model Example {
```

#### Soft delete: `deleted_at`
By default, if your field's name is exactly `deleted_at`, you don't need to add this annotation.
By default, if your field's name is exactly `deleted_at`, you don't need to add these annotations since they're automatically classified as deleted-at timestamp and considered read-only.

When `deleted_at` is defined, the model will automatically implement the `SoftDelete` trait.

Fields with this annotation are automatically considered as read-only.

```prisma
model Example {
id Int @id @default(autoincrement())
Expand Down Expand Up @@ -717,6 +745,8 @@ model Example {
#### Read only: `read-only`
Set the field as read-only (via PHPDocs). If the field is a DateTime, instead of a `Carbon` instance, it will be casted to `CarbonImmutable`.

By default, fields called `created_at`, `updated_at` and `deleted_at` are considered as read-only.

```prisma
model Example {
id Int @id @default(autoincrement())
Expand Down Expand Up @@ -772,6 +802,19 @@ enum Enum {
}
```

#### Path: `path`
If you need to export a specific enum to a different folder to the one defined in your generator settings as `prismaEnumsPath` (see [generator settings](#generator-settings)),you can use this annotation. The path is relative to the `output` path defined in your generator settings.

**Warning:** If you remove an enum with a custom path from your schema, the generated PHP file has to be deleted manually.

```prisma
/// path:./app/Enums/domain
enum Enum {
A
B
}
```

### Relations
Here you can find some examples on how to define different type of relations. If you need more information about what is happening, look at the [official documentation](https://www.prisma.io/docs/concepts/components/prisma-schema/relations).

Expand Down
1 change: 1 addition & 0 deletions packages/generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"type-check": "tsc --project src/tsconfig.json --pretty --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"prepack": "yarn build"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/generator/src/__tests__/__fixtures__/get-sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const getMassAssignableConflictSample = async () =>
readSample('mass-assignable-conflict.prisma');
export const getMultipleInheritanceSample = async () =>
readSample('multiple-inheritance.prisma');
export const getMultiplePathsSample = async () =>
readSample('multiple-paths.prisma');
export const getEmptySample = async () => readSample('empty.prisma');
export const getMongoSample = async () => readSample('mongo.prisma');
export const getMySqlSample = async () => readSample('mysql.prisma');
Expand All @@ -46,6 +48,7 @@ export default {
getHiddenVisibleConflictSample,
getMassAssignableConflictSample,
getMultipleInheritanceSample,
getMultiplePathsSample,
getMongoSample,
getMySqlSample,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

/// path:./app/Models/a
/// path:./app/Models/b
model MultiplePaths {
id Int @id @default(autoincrement())
}

/// path:./app/Enums/a
/// path:./app/Enums/b
enum MultiplePathsEnum {
A @map("1")
B @map("2")
}
17 changes: 17 additions & 0 deletions packages/generator/src/__tests__/__fixtures__/sample.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ model PivotWithCustomExtend {
id Int @id @default(autoincrement())
}

/// path:./app/Models/custom
model ModelCustomPath {
id Int @id @default(autoincrement())
}

/// path:./app/Enums/custom
enum EnumCustomPath {
A @map("1")
B @map("2")
}

model WithEnum {
id Int @id @default(autoincrement())
enum Enum
Expand Down Expand Up @@ -245,6 +256,12 @@ model DefaultValues {
role Role @default(USER)
}

model ReadonlyDatetime {
id Int @id @default(autoincrement())
readonly DateTime /// read-only
editable DateTime
}

model DefaultValuesWithoutMethodCall {
id Int @id
email String @default("hello@world.test")
Expand Down
2 changes: 2 additions & 0 deletions packages/generator/src/__tests__/errors/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MassAssignableConflictError from '../../errors/mass-assignable-conflict-e
import ModelNotFoundError from '../../errors/model-not-found-error';
import MultipleDataSourcesError from '../../errors/multiple-data-sources-error';
import MultipleInheritanceError from '../../errors/multiple-inheritance-error';
import TooManyPathsError from '../../errors/too-many-paths-error';

const errors = [
CompositeKeyError,
Expand All @@ -24,6 +25,7 @@ const errors = [
ModelNotFoundError,
MultipleDataSourcesError,
MultipleInheritanceError,
TooManyPathsError,
];

test.each(errors)('Error: %s', ErrorClass => {
Expand Down
Loading

0 comments on commit b818093

Please sign in to comment.