firstly 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/esm/bin/cmd.js +1 -156
  3. package/esm/changeLog/index.d.ts +1 -6
  4. package/esm/internals/BaseEnum.d.ts +1 -1
  5. package/esm/internals/FF_Entity.js +1 -17
  6. package/esm/internals/cellsBuildor.js +5 -4
  7. package/esm/internals/index.d.ts +2 -6
  8. package/esm/internals/storeItem.d.ts +12 -21
  9. package/esm/internals/storeItem.js +20 -6
  10. package/esm/svelte/FF_Repo.svelte.d.ts +9 -0
  11. package/esm/svelte/FF_Repo.svelte.js +39 -0
  12. package/esm/svelte/class/SP.svelte.js +14 -2
  13. package/esm/ui/Button.svelte +4 -52
  14. package/esm/ui/Button.svelte.d.ts +0 -2
  15. package/esm/ui/Field.svelte +10 -1
  16. package/esm/ui/Grid.svelte +2 -5
  17. package/esm/ui/Grid2.svelte +1 -4
  18. package/esm/ui/internals/select/MultiSelectMelt.svelte +4 -4
  19. package/esm/ui/internals/select/MultiSelectMelt.svelte.d.ts +1 -1
  20. package/esm/ui/internals/select/SelectMelt.svelte +28 -19
  21. package/esm/ui/internals/select/SelectMelt.svelte.d.ts +1 -1
  22. package/esm/ui/internals/select/SelectRadio.svelte +1 -1
  23. package/esm/ui/internals/select/SelectRadio.svelte.d.ts +1 -1
  24. package/package.json +6 -19
  25. package/esm/auth/AuthController.d.ts +0 -58
  26. package/esm/auth/AuthController.js +0 -114
  27. package/esm/auth/Entities.d.ts +0 -47
  28. package/esm/auth/Entities.js +0 -182
  29. package/esm/auth/README.md +0 -3
  30. package/esm/auth/index.d.ts +0 -5
  31. package/esm/auth/index.js +0 -5
  32. package/esm/auth/server/AuthController.server.d.ts +0 -58
  33. package/esm/auth/server/AuthController.server.js +0 -518
  34. package/esm/auth/server/handleAuth.d.ts +0 -4
  35. package/esm/auth/server/handleAuth.js +0 -142
  36. package/esm/auth/server/handleGuard.d.ts +0 -22
  37. package/esm/auth/server/handleGuard.js +0 -34
  38. package/esm/auth/server/helperDb.d.ts +0 -10
  39. package/esm/auth/server/helperDb.js +0 -56
  40. package/esm/auth/server/helperFirstly.d.ts +0 -1
  41. package/esm/auth/server/helperFirstly.js +0 -9
  42. package/esm/auth/server/helperOslo.d.ts +0 -7
  43. package/esm/auth/server/helperOslo.js +0 -24
  44. package/esm/auth/server/helperRemultServer.d.ts +0 -5
  45. package/esm/auth/server/helperRemultServer.js +0 -44
  46. package/esm/auth/server/helperRole.d.ts +0 -19
  47. package/esm/auth/server/helperRole.js +0 -57
  48. package/esm/auth/server/index.d.ts +0 -8
  49. package/esm/auth/server/index.js +0 -8
  50. package/esm/auth/server/module.d.ts +0 -300
  51. package/esm/auth/server/module.js +0 -230
  52. package/esm/auth/server/providers/github.d.ts +0 -33
  53. package/esm/auth/server/providers/github.js +0 -87
  54. package/esm/auth/server/providers/helperProvider.d.ts +0 -1
  55. package/esm/auth/server/providers/helperProvider.js +0 -25
  56. package/esm/auth/static/assets/Page-BHW08QWz.css +0 -1
  57. package/esm/auth/static/assets/Page-BRNWcY5Z.d.ts +0 -2
  58. package/esm/auth/static/assets/Page-BRNWcY5Z.js +0 -1
  59. package/esm/auth/static/assets/Page-CFcEsGK8.d.ts +0 -2
  60. package/esm/auth/static/assets/Page-CFcEsGK8.js +0 -7
  61. package/esm/auth/static/assets/Page-tLVs5slF.d.ts +0 -2
  62. package/esm/auth/static/assets/Page-tLVs5slF.js +0 -1
  63. package/esm/auth/static/assets/index-D38rqu4x.d.ts +0 -201
  64. package/esm/auth/static/assets/index-D38rqu4x.js +0 -2
  65. package/esm/auth/static/assets/index-DKWpA6v7.css +0 -4
  66. package/esm/auth/static/favicon.svg +0 -79
  67. package/esm/auth/static/index.html +0 -13
  68. package/esm/auth/types.d.ts +0 -73
  69. package/esm/auth/types.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # firstly
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#232](https://github.com/jycouet/firstly/pull/232) [`fec8bc0`](https://github.com/jycouet/firstly/commit/fec8bc088733d63ce6752b0a764b786f80b736cb) Thanks [@jycouet](https://github.com/jycouet)! - BREAKING: Remove deprecated lucia-style auth module (`firstly/auth`, `firstly/auth/server`).
8
+
9
+ Migrate to `better-auth` (see remult docs). Removed deps: `@oslojs/*`, `arctic`, `bcryptjs`.
10
+ Also removed `packages/ui` (was only used for the auth UI).
11
+
12
+ ### Patch Changes
13
+
14
+ - [#221](https://github.com/jycouet/firstly/pull/221) [`0b08040`](https://github.com/jycouet/firstly/commit/0b0804001c9a5bdff560fbcbb8b511c626d260f8) Thanks [@jycouet](https://github.com/jycouet)! - bump deps
15
+
3
16
  ## 0.2.1
4
17
 
5
18
  ### Patch Changes
package/esm/bin/cmd.js CHANGED
@@ -11,7 +11,6 @@ const pkg = JSON.parse(read('./package.json') ?? '{}');
11
11
  const version = pkg.devDependencies?.['firstly'] ?? pkg.dependencies?.['firstly'] ?? '???';
12
12
  console.info('');
13
13
  p.intro(`${green(`⚡️`)} Welcome to firstly world! ${gray(` - v${version}`)}`);
14
- const keys = ['all', 'module-demo', 'dependencies'];
15
14
  const options = [
16
15
  {
17
16
  value: 'all',
@@ -142,11 +141,6 @@ export default {
142
141
  '.env.example': [
143
142
  `# Enable some roles
144
143
  # FF_ADMIN = 'JYC'
145
- # FF_AUTH_ADMIN = ''
146
-
147
- # Enable GitHub login
148
- # GITHUB_CLIENT_ID = ''
149
- # GITHUB_CLIENT_SECRET = ''
150
144
  `,
151
145
  ],
152
146
  './tsconfig.json': [
@@ -185,7 +179,6 @@ export default defineConfig({
185
179
  firstly<KIT_ROUTES>({
186
180
  kitRoutes: {
187
181
  LINKS: {
188
- login: 'ff/auth/sign-in',
189
182
  github: 'https://github.com/[owner]/[repo]',
190
183
  remult_admin: 'api/admin',
191
184
  },
@@ -198,13 +191,9 @@ export default defineConfig({
198
191
  ],
199
192
  './svelte.config.js': [
200
193
  `import adapter from '@sveltejs/adapter-auto'
201
- import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
202
194
 
203
195
  /** @type {import('@sveltejs/kit').Config} */
204
196
  const config = {
205
- // Consult https://svelte.dev/docs/kit/integrations
206
- // for more information about preprocessors
207
- preprocess: vitePreprocess(),
208
197
 
209
198
  kit: {
210
199
  // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
@@ -251,7 +240,6 @@ export { }
251
240
  './src/server/index.ts': [
252
241
  `import { FF_Role } from '../internals'
253
242
  import { firstly, Module } from '../server'
254
- import { auth } from '../auth/server'
255
243
  import { mail } from '../mail/server'
256
244
  import { changeLog } from '../changeLog/server'
257
245
 
@@ -259,44 +247,7 @@ import { log, Role } from '${libAlias}'
259
247
  import { task } from '${modulesAlias}/task/server'
260
248
 
261
249
  export const api = firstly({
262
- //----------------------------------------
263
- // To switch to postgres
264
- // NEEDS ON TOP OF THE FILE:
265
- // import { createPostgresConnection } from 'remult/postgres'
266
- // import { DATABASE_URL } from '$env/static/private'
267
- //----------------------------------------
268
- // dataProvider: await createPostgresConnection({
269
- // connectionString: DATABASE_URL,
270
- // }),
271
-
272
250
  modules: [
273
- //----------------------------------------
274
- // Core Module: auth
275
- //----------------------------------------
276
- auth({
277
- providers: {
278
- demo: [
279
- { name: 'Ermin' },
280
- { name: 'JYC', roles: [FF_Role.FF_Role_Admin] },
281
- { name: 'Noam', roles: [FF_Role.FF_Role_Admin, Role.Boss] },
282
- ],
283
-
284
- // password: {},
285
-
286
- // otp: {},
287
-
288
- oAuths: [
289
- //----------------------------------------
290
- // To enable OAuth via Github
291
- // Instructions by hovering the method \`github\`
292
- // NEEDS ON TOP OF THE FILE:
293
- // import { github } from '../auth/server'
294
- //----------------------------------------
295
- // github(),
296
- ],
297
- },
298
- }),
299
-
300
251
  //----------------------------------------
301
252
  // Core Module: mail
302
253
  //----------------------------------------
@@ -331,25 +282,11 @@ export const api = firstly({
331
282
  ],
332
283
  './src/hooks.server.ts': [
333
284
  `import { sequence } from '@sveltejs/kit/hooks'
334
- import { redirect } from '@sveltejs/kit'
335
285
 
336
- import { handleAuth, handleGuard } from '../auth/server'
337
- import { route } from '${libAlias}/ROUTES'
338
286
  import { api as handleRemult } from '${serverAlias}'
339
287
 
340
288
  export const handle = sequence(
341
- //
342
289
  handleRemult,
343
- handleAuth,
344
- // client side guard is not here!
345
- handleGuard({
346
- authenticated: ['/app*'],
347
- redirectToLogin: route('/'),
348
- // You want to redirect to the firstly UI ? change redirectToLogin to this 👇
349
- // redirectToLogin: route('login'),
350
- redirectAuthenticated: route('/app'),
351
- redirect,
352
- })
353
290
  )
354
291
  `,
355
292
  ],
@@ -390,8 +327,6 @@ export const load = (async (event) => {
390
327
  import { Remult, remult } from 'remult'
391
328
 
392
329
  import { route } from '${libAlias}/ROUTES'
393
- import SignIn from '${libAlias}/ui/SignIn.svelte'
394
- import SignOut from '${libAlias}/ui/SignOut.svelte'
395
330
 
396
331
  import type { LayoutData } from './$types'
397
332
 
@@ -453,26 +388,10 @@ import type { LayoutData } from './$types'
453
388
  <h1>${pkg.name}</h1>
454
389
 
455
390
  {#if remult.authenticated()}
456
- <div style="float:right;">
457
- <SignOut></SignOut>
458
- </div>
459
391
  <span>{remult.user?.name} ({remult.user?.roles})<br /><br /></span>
460
- {:else}
461
- <SignIn demo="Ermin"></SignIn>
462
- <SignIn demo="JYC"></SignIn>
463
- <SignIn demo="Noam"></SignIn>
464
- <br />
465
- <SignIn ffLink></SignIn>
466
- <br />
467
- <SignIn oauth="github"></SignIn>
468
392
  {/if}
469
393
 
470
- <hr />
471
-
472
- <a href={route('/')}>Home</a> |
473
- {#if remult.authenticated()}
474
- <a href={route('/app')}>App (Protected route)</a> |
475
- {/if}
394
+ <a href={route('/')}>Home</a> |
476
395
  <a href={route('/demo/task')}>Demo task</a>
477
396
 
478
397
  <hr />
@@ -523,7 +442,6 @@ export const load = (async (event) => {
523
442
  // Lib files
524
443
  './src/lib/index.ts': [
525
444
  `import { FF_Role } from '../internals'
526
- import { FF_Role_Auth } from '../auth'
527
445
  import { Log } from '@kitql/helpers'
528
446
 
529
447
  /**
@@ -536,81 +454,8 @@ export const log = new Log('${pkg.name}')
536
454
  */
537
455
  export const Role = {
538
456
  Boss: 'Boss',
539
- ...FF_Role_Auth,
540
457
  ...FF_Role,
541
458
  } as const
542
- `,
543
- ],
544
- './src/lib/ui/SignIn.svelte': [
545
- `<script lang="ts">
546
- import { isError } from '../internals'
547
- import { AuthController } from '../auth'
548
-
549
- import { goto, invalidateAll } from '$app/navigation'
550
-
551
- import { route } from '${libAlias}/ROUTES'
552
-
553
- // Examples of signin modes
554
- export let demo = ''
555
- export let ffLink = false
556
- export let oauth: 'github' | undefined = undefined
557
-
558
- const signinDemo = async (identif: string) => {
559
- try {
560
- await AuthController.signInDemo(identif)
561
- invalidateAll()
562
- } catch (error) {
563
- if (isError(error)) {
564
- // You will probably not leave this alert in production
565
- alert(error.message)
566
- }
567
- }
568
- }
569
-
570
- async function signinOAuth(provider: 'github') {
571
- try {
572
- window.location.href = await AuthController.signInOAuthGetUrl({
573
- provider,
574
- redirect: window.location.pathname,
575
- })
576
- } catch (error) {
577
- if (isError(error)) {
578
- // You will probably not leave this alert in production
579
- alert(error.message)
580
- }
581
- }
582
- }
583
- </script>
584
-
585
- {#if demo}
586
- <button on:click={() => signinDemo(demo)}>Login as {demo}</button>
587
- {:else if ffLink}
588
- <button on:click={() => goto(route('login'))}>Login with Firstly UI</button>
589
- {:else if oauth}
590
- <button on:click={() => signinOAuth(oauth)}>Login With {oauth}</button>
591
- {/if}
592
- `,
593
- ],
594
- './src/lib/ui/SignOut.svelte': [
595
- `<script lang="ts">
596
- import { isError } from '../internals'
597
- import { AuthController } from '../auth'
598
-
599
- import { invalidateAll } from '$app/navigation'
600
-
601
- const logout = async () => {
602
- try {
603
- await AuthController.signOut()
604
- invalidateAll()
605
- } catch (error) {
606
- if (isError(error)) {
607
- alert(error.message)
608
- }
609
- }
610
- }
611
- </script>
612
-
613
- <button on:click={logout}>Logout</button>
614
459
  `,
615
460
  ],
616
461
  // Task module
@@ -45,15 +45,10 @@ export declare const withChangeLog: <entityType>(options?: EntityOptions<entityT
45
45
  entityRefInit?: ((ref: import("remult").EntityRef<entityType>, row: entityType) => void) | undefined;
46
46
  apiRequireId?: import("remult").Allowed;
47
47
  dataProvider?: ((defaultDataProvider: import("remult").DataProvider) => import("remult").DataProvider | Promise<import("remult").DataProvider> | undefined | null) | undefined;
48
+ changeLog?: false | ColumnDeciderArgs<entityType> | undefined;
48
49
  ui?: {
49
50
  getLayout?: import("../svelte/customField").getLayout<entityType> | undefined;
50
51
  } | undefined;
51
52
  searchableFind?: ((str: string) => import("remult").FindOptionsBase<entityType>) | undefined;
52
53
  displayValue?: ((item: entityType) => import("../internals").BaseItem) | undefined;
53
- permissionApiCrud?: import("../internals").BaseEnum[] | import("../internals").BaseEnum;
54
- permissionApiDelete?: import("../internals").BaseEnum[] | import("../internals").BaseEnum;
55
- permissionApiInsert?: import("../internals").BaseEnum[] | import("../internals").BaseEnum;
56
- permissionApiRead?: import("../internals").BaseEnum[] | import("../internals").BaseEnum;
57
- permissionApiUpdate?: import("../internals").BaseEnum[] | import("../internals").BaseEnum;
58
- changeLog?: false | ColumnDeciderArgs<entityType> | undefined;
59
54
  };
@@ -8,7 +8,7 @@ export type FF_Icon = {
8
8
  caption?: string;
9
9
  };
10
10
  export type BaseItem = BaseEnumOptions & {
11
- id: string;
11
+ id: string | null;
12
12
  captionSub?: string | (string | undefined)[];
13
13
  href?: string;
14
14
  repo?: Repository<any>;
@@ -1,21 +1,5 @@
1
1
  import { Entity } from 'remult';
2
2
  import { withChangeLog } from '../changeLog';
3
- const toAllow = (permission) => {
4
- if (permission) {
5
- if (Array.isArray(permission)) {
6
- return permission.map((p) => p.id);
7
- }
8
- return permission.id;
9
- }
10
- return undefined;
11
- };
12
3
  export function FF_Entity(key, options) {
13
- return Entity(key, withChangeLog({
14
- ...options,
15
- allowApiCrud: options?.allowApiCrud ?? toAllow(options?.permissionApiCrud),
16
- allowApiDelete: options?.allowApiDelete ?? toAllow(options?.permissionApiDelete),
17
- allowApiInsert: options?.allowApiInsert ?? toAllow(options?.permissionApiInsert),
18
- allowApiRead: options?.allowApiRead ?? toAllow(options?.permissionApiRead),
19
- allowApiUpdate: options?.allowApiUpdate ?? toAllow(options?.permissionApiUpdate),
20
- }));
4
+ return Entity(key, withChangeLog({ ...options }));
21
5
  }
@@ -98,9 +98,10 @@ export const buildWhere = (entity, defaultWhere, fields_filter, fields_search, o
98
98
  and.push(...buildSearchWhere(entity, fields_search, obj.search));
99
99
  }
100
100
  for (const field of fields_filter) {
101
- // if there is a value
102
- if (obj && obj[field.key]) {
103
- const rfi = getRelationFieldInfo(field);
101
+ const rfi = getRelationFieldInfo(field);
102
+ // For relation fields, allow null as valid filter value (filters for NULL in DB)
103
+ const hasValue = rfi?.type === 'toOne' ? obj && obj[field.key] !== undefined : obj && obj[field.key];
104
+ if (hasValue) {
104
105
  if (field.inputType === 'checkbox') {
105
106
  // @ts-ignore
106
107
  and.push({ [field.key]: obj[field.key] });
@@ -127,7 +128,7 @@ export const buildWhere = (entity, defaultWhere, fields_filter, fields_search, o
127
128
  }
128
129
  }
129
130
  else if (rfi?.type === 'toOne') {
130
- // @ts-ignore (setting the id of the relation)
131
+ // @ts-ignore (setting the id of the relation, null = filter for NULL)
131
132
  and.push({ [field.key]: obj[field.key] });
132
133
  }
133
134
  else {
@@ -18,7 +18,7 @@ import { default as Link } from '../ui/link/Link.svelte';
18
18
  import { default as LinkPlus } from '../ui/link/LinkPlus.svelte';
19
19
  import { default as Loading } from '../ui/Loading.svelte';
20
20
  import { default as Tooltip } from '../ui/Tooltip.svelte';
21
- import type { BaseEnum, BaseItem, BaseItemLight, FF_Icon } from './BaseEnum.js';
21
+ import type { BaseItem, BaseItemLight, FF_Icon } from './BaseEnum.js';
22
22
  import type { CellsInput } from './cellsBuildor.js';
23
23
  import { FF_Role } from './common.js';
24
24
  import { storeItem, type StoreItem } from './storeItem.js';
@@ -67,16 +67,12 @@ declare module 'remult' {
67
67
  };
68
68
  default_select_if_one_item?: boolean;
69
69
  multiSelect?: boolean;
70
+ filterNobodyLabel?: string;
70
71
  skipForDefaultField?: boolean;
71
72
  }
72
73
  interface EntityOptions<entityType> {
73
74
  searchableFind?: (str: string) => FindOptionsBase<entityType>;
74
75
  displayValue?: (item: entityType) => BaseItem;
75
- permissionApiCrud?: BaseEnum[] | BaseEnum;
76
- permissionApiDelete?: BaseEnum[] | BaseEnum;
77
- permissionApiInsert?: BaseEnum[] | BaseEnum;
78
- permissionApiRead?: BaseEnum[] | BaseEnum;
79
- permissionApiUpdate?: BaseEnum[] | BaseEnum;
80
76
  changeLog?: false | ColumnDeciderArgs<entityType>;
81
77
  }
82
78
  }
@@ -1,28 +1,19 @@
1
- import type { ErrorInfo, FindOptions, Repository } from 'remult';
2
- export type StoreItem<T> = ReturnType<typeof storeItem<T>>;
3
- type TheStoreItem<T> = {
4
- item: T | undefined;
5
- loading?: boolean;
6
- errors: ErrorInfo<T> | undefined;
7
- globalError?: string | undefined;
8
- };
9
- export declare const storeItem: <T>(repo: Repository<T>, initValues?: TheStoreItem<T>) => {
10
- subscribe: (this: void, run: import("svelte/store").Subscriber<TheStoreItem<T>>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
1
+ import type { EntityFilter, ErrorInfo, FindOptions, Repository } from 'remult';
2
+ export interface StoreItem<T> {
3
+ subscribe: (run: (value: TheStoreItem<T>) => void) => () => void;
11
4
  create: (item: Partial<T>) => void;
12
5
  set: (newItem: TheStoreItem<T>) => void;
13
- /**
14
- * if you have keys, build the id with
15
- * ```ts
16
- * const id = repo.metadata.idMetadata.getId({a:1,b:2})
17
- * store.fetch(id)
18
- * ```
19
- */
20
- fetch: (id: Parameters<Repository<T>["findId"]>[0], options?: FindOptions<T>, onNewData?: (item: T) => void) => Promise<void>;
21
- /**
22
- * `.save()` will `update` or `insert` the current item.
23
- */
6
+ fetch: (idOrWhere: string | number | EntityFilter<T>, options?: FindOptions<T>, onNewData?: (item: T | undefined) => void) => Promise<void>;
24
7
  save: () => Promise<T | undefined>;
25
8
  delete: () => Promise<void>;
26
9
  onChange: (prop: keyof T, callback: (newValue: any, oldValue: any) => void) => void;
10
+ }
11
+ type TheStoreItem<T> = {
12
+ item: T | undefined;
13
+ loading?: boolean;
14
+ initLoading?: boolean;
15
+ errors: ErrorInfo<T> | undefined;
16
+ globalError?: string | undefined;
27
17
  };
18
+ export declare const storeItem: <T>(repo: Repository<T>, initValues?: TheStoreItem<T>) => StoreItem<T>;
28
19
  export {};
@@ -5,6 +5,7 @@ import { isError } from './helper.js';
5
5
  export const storeItem = (repo, initValues = {
6
6
  item: undefined,
7
7
  loading: true,
8
+ initLoading: true,
8
9
  errors: undefined,
9
10
  globalError: undefined,
10
11
  }) => {
@@ -30,38 +31,50 @@ export const storeItem = (repo, initValues = {
30
31
  internalStore.set({
31
32
  item: repo.create(item),
32
33
  loading: false,
34
+ initLoading: false,
33
35
  errors: {},
34
36
  globalError: undefined,
35
37
  });
36
38
  },
37
39
  // set: internalStore.set,
38
40
  set: (newItem) => {
39
- internalStore.update((s) => {
41
+ internalStore.update(() => {
40
42
  return { ...newItem };
41
43
  });
42
44
  },
43
45
  /**
44
- * if you have keys, build the id with
46
+ * Fetch by ID or WHERE clause
45
47
  * ```ts
48
+ * // By ID (string or number)
49
+ * store.fetch(123)
50
+ * store.fetch('abc')
51
+ *
52
+ * // By WHERE clause (object)
53
+ * store.fetch({ siteId: 123 })
54
+ *
55
+ * // With composite keys, build the id with
46
56
  * const id = repo.metadata.idMetadata.getId({a:1,b:2})
47
57
  * store.fetch(id)
48
58
  * ```
49
59
  */
50
- fetch: async (id, options, onNewData) => {
60
+ fetch: async (idOrWhere, options, onNewData) => {
51
61
  if (BROWSER) {
52
62
  internalStore.update((s) => ({ ...s, loading: true }));
53
63
  try {
54
- const item = await repo.findId(id, options);
55
- // lastOptions = options
64
+ const isId = typeof idOrWhere === 'string' || typeof idOrWhere === 'number';
65
+ const item = isId
66
+ ? await repo.findId(idOrWhere, options)
67
+ : await repo.findFirst(idOrWhere, options);
56
68
  internalStore.update((s) => ({
57
69
  ...s,
58
70
  loading: false,
71
+ initLoading: false,
59
72
  item: item ?? {},
60
73
  errors: undefined,
61
74
  globalError: undefined,
62
75
  }));
63
76
  if (onNewData) {
64
- onNewData(item ?? {});
77
+ onNewData(item ?? undefined);
65
78
  }
66
79
  }
67
80
  catch (error) {
@@ -69,6 +82,7 @@ export const storeItem = (repo, initValues = {
69
82
  internalStore.update((s) => ({
70
83
  ...s,
71
84
  loading: false,
85
+ initLoading: false,
72
86
  item: {},
73
87
  errors: {},
74
88
  // @ts-ignore
@@ -62,6 +62,15 @@ export declare class FF_Repo<Entity, QueryOptions extends QueryOptionsHelper<Ent
62
62
  aggregates: ExtractAggregateResult<Entity, QueryOptions>;
63
63
  hasNextPage: boolean;
64
64
  } | undefined>;
65
+ /**
66
+ * Refresh query keeping current items count (BIG refresh)
67
+ * Useful after edit/delete to stay at current scroll position
68
+ */
69
+ queryRefresh(options: Pick<QueryOptionsHelper<Entity>, 'where' | 'orderBy'>): Promise<{
70
+ items: Entity[];
71
+ aggregates: ExtractAggregateResult<Entity, QueryOptions>;
72
+ hasNextPage: boolean;
73
+ } | undefined>;
65
74
  create(...args: Parameters<Repository<Entity>['create']>): Entity;
66
75
  delete(...args: Parameters<Repository<Entity>['delete']>): Promise<undefined>;
67
76
  getLayout: getLayoutStrict<Entity>;
@@ -132,6 +132,45 @@ export class FF_Repo {
132
132
  hasNextPage: this.hasNextPage,
133
133
  });
134
134
  }
135
+ /**
136
+ * Refresh query keeping current items count (BIG refresh)
137
+ * Useful after edit/delete to stay at current scroll position
138
+ */
139
+ async queryRefresh(options) {
140
+ const currentCount = this.items?.length ?? this.#queryOptions?.pageSize ?? 25;
141
+ this.loading = {
142
+ ...this.loading,
143
+ fetching: true,
144
+ init: this.items === undefined,
145
+ };
146
+ const { data: queryResult, error: queryResultError } = tryCatchSync(() => this.#repo.query({
147
+ ...this.#queryOptions,
148
+ ...options,
149
+ pageSize: currentCount,
150
+ aggregate: {
151
+ ...this.#queryOptions?.aggregate,
152
+ },
153
+ }));
154
+ if (queryResultError) {
155
+ this.globalError = queryResultError.message;
156
+ return this.loadingEnd();
157
+ }
158
+ const { data: paginator, error: paginatorError } = await tryCatch(queryResult.paginator());
159
+ if (paginatorError) {
160
+ this.globalError = paginatorError.message;
161
+ return this.loadingEnd();
162
+ }
163
+ this.#paginator = paginator;
164
+ this.items = this.#paginator.items;
165
+ // @ts-expect-error - We know the structure will match due to how we define the types
166
+ this.aggregates = this.#paginator.aggregates;
167
+ this.hasNextPage = this.#paginator.hasNextPage && this.aggregates.$count > this.items.length;
168
+ return this.loadingEnd({
169
+ items: this.items,
170
+ aggregates: this.aggregates,
171
+ hasNextPage: this.hasNextPage,
172
+ });
173
+ }
135
174
  create(...args) {
136
175
  this.item = this.#repo.create(...args);
137
176
  return this.item;
@@ -7,6 +7,7 @@ import { goto } from '$app/navigation';
7
7
  import { page } from '$app/state';
8
8
  import { debounce } from '../helpers/debounce.js';
9
9
  const CONFIG_DELIMITER = ';';
10
+ const NULL_URL_VALUE = '__null__';
10
11
  /**
11
12
  * SearchParams class for handling URL search parameters with Svelte 5 runes
12
13
  * Provides automatic binding and URL updates
@@ -257,6 +258,12 @@ export class SP {
257
258
  const urlKey = this.keyMap[propKey]; // Get the URL parameter key
258
259
  const paramValue = params.get(urlKey);
259
260
  if (paramValue !== null) {
261
+ // Decode null sentinel from URL
262
+ if (paramValue === NULL_URL_VALUE) {
263
+ this.paramValues[propKey] = null;
264
+ this.debouncedValues[propKey] = null;
265
+ continue;
266
+ }
260
267
  // If there is a decode function, always use it to get the proper object
261
268
  if (def.decode) {
262
269
  const decodedValue = def.decode(paramValue);
@@ -314,11 +321,16 @@ export class SP {
314
321
  }
315
322
  const params = new URLSearchParams(window.location.search);
316
323
  for (const [propKey, value] of Object.entries(this.debouncedValues)) {
317
- // Skip undefined or null values
318
- if (value === undefined || value === null) {
324
+ // Skip undefined values
325
+ if (value === undefined) {
319
326
  params.delete(this.keyMap[propKey]);
320
327
  continue;
321
328
  }
329
+ // Encode null as special URL value
330
+ if (value === null) {
331
+ params.set(this.keyMap[propKey], NULL_URL_VALUE);
332
+ continue;
333
+ }
322
334
  // Get the definition and URL key
323
335
  const def = this.config[propKey];
324
336
  if (!def)