diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 81a3c1e..6ddfd59 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,6 +29,7 @@ jobs: - run: npm ci - run: npm run lint - run: npm run build + - run: npm run test:offline-types:nonstrict test: runs-on: ubuntu-latest strategy: diff --git a/package.json b/package.json index 1fea930..35e313d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lint": "ng lint", "test:watch": "ng test", "test:actions": "node --test .github/actions/classify-mobile-release/classify-mobile-release.spec.mjs && vitest run --config .github/actions/vitest.config.mjs", + "test:offline-types:nonstrict": "npm run prebuild:kit && tsc -p projects/kit/offline/tsconfig.nonstrict-null.json", "e2e": "playwright test", "e2e:ui": "playwright test --ui" }, diff --git a/projects/kit/offline/src/lib/offline-replica-schema.ts b/projects/kit/offline/src/lib/offline-replica-schema.ts index 18fc743..80ccae6 100644 --- a/projects/kit/offline/src/lib/offline-replica-schema.ts +++ b/projects/kit/offline/src/lib/offline-replica-schema.ts @@ -97,14 +97,20 @@ type NormalizeReplicaColumnValue = T extends string ? string | Date : T; -type IsNullableSelectValue = null extends T ? true : undefined extends T ? true : false; - -type OfflineReplicaColumnDefForValue = IsNullableSelectValue extends true - ? OfflineReplicaColumnDef< - NormalizeReplicaColumnValue>, - { readonly [replicaNullableBrand]: 'nullable' } - > - : OfflineReplicaColumnDef, { readonly [replicaNullableBrand]: 'required' }>; +type StrictNullChecksEnabled = undefined extends null ? false : true; + +type ReplicaColumnNullabilityForSelect = StrictNullChecksEnabled extends true + ? null extends T + ? ReplicaNullableBrand & { readonly [replicaNullableBrand]: 'nullable' } + : undefined extends T + ? ReplicaNullableBrand & { readonly [replicaNullableBrand]: 'nullable' } + : ReplicaNullableBrand & { readonly [replicaNullableBrand]: 'required' } + : ReplicaNullableBrand; + +type OfflineReplicaColumnDefForValue = OfflineReplicaColumnDef< + NormalizeReplicaColumnValue>, + ReplicaColumnNullabilityForSelect +>; type OfflineReplicaFieldDefForKey, K extends keyof TSelect> = | (StripNullish extends number ? OfflineReplicaServerIdDef : never) diff --git a/projects/kit/offline/tsconfig.nonstrict-null.json b/projects/kit/offline/tsconfig.nonstrict-null.json new file mode 100644 index 0000000..daebe5c --- /dev/null +++ b/projects/kit/offline/tsconfig.nonstrict-null.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "es2022", + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": false, + "target": "es2022" + }, + "files": ["type-tests/nonstrict-null.ts"] +} diff --git a/projects/kit/offline/type-tests/nonstrict-null.ts b/projects/kit/offline/type-tests/nonstrict-null.ts new file mode 100644 index 0000000..5508775 --- /dev/null +++ b/projects/kit/offline/type-tests/nonstrict-null.ts @@ -0,0 +1,74 @@ +import { + defineOfflineReplicaSchema, + defineReplicaEntity, + integer, + nullable, + serverId, + text, +} from '../../../../dist/kit/types/rdlabo-ionic-angular-kit-offline'; + +// Type alias intentionally exercises Record compatibility in a non-strict-null consumer. +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type NonStrictSelect = { + id: number; + title: string; + subtitle: string | null; + sortOrder: number; +}; + +const entity = defineReplicaEntity()({ + table: 'nonstrict_items', + sourceKey: 'nonstrict_items', + scope: 'user', + fields: { + id: serverId(), + title: text(), + subtitle: nullable(text()), + sortOrder: integer(), + }, +}); + +defineOfflineReplicaSchema({ + version: 1, + entities: [entity], + migrations: [], +}); + +defineReplicaEntity()({ + table: 'nonstrict_invalid_items', + sourceKey: 'nonstrict_invalid_items', + scope: 'user', + fields: { + id: serverId(), + // @ts-expect-error Primitive affinity checks remain active without strictNullChecks. + title: integer(), + subtitle: nullable(text()), + sortOrder: integer(), + }, +}); + +defineReplicaEntity()({ + table: 'nonstrict_missing_items', + sourceKey: 'nonstrict_missing_items', + scope: 'user', + // @ts-expect-error Exact-key validation still rejects a missing select property. + fields: { + id: serverId(), + title: text(), + subtitle: nullable(text()), + }, +}); + +defineReplicaEntity()({ + table: 'nonstrict_extra_items', + sourceKey: 'nonstrict_extra_items', + scope: 'user', + // @ts-expect-error Exact-key validation still rejects an unknown property. + fields: { + id: serverId(), + title: text(), + subtitle: nullable(text()), + sortOrder: integer(), + extra: text(), + }, +});