@delta-comic/db 3.0.0-next.10 → 3.0.0-next.12

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.
package/dist/index.mjs CHANGED
@@ -4,9 +4,11 @@ import { isTauri } from "@tauri-apps/api/core";
4
4
  import { CamelCasePlugin, Kysely } from "kysely";
5
5
  import { SerializePlugin } from "kysely-plugin-serialize";
6
6
  import { defineMutation, useMutation, useQuery, useQueryCache } from "@pinia/colada";
7
+ import { ref, toValue, watch } from "vue";
7
8
  import { SourcedValue, Struct, UniContentPage } from "@delta-comic/model";
8
9
  import { fromPairs } from "es-toolkit/compat";
9
- import { ref, toValue, watch } from "vue";
10
+ import { Compile } from "typebox/compile";
11
+ import { Value } from "typebox/value";
10
12
  //#region lib/utils.ts
11
13
  var utils_exports = /* @__PURE__ */ __exportAll({
12
14
  CommonQueryKey: () => CommonQueryKey,
@@ -43,7 +45,6 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
43
45
  removeByNames: () => removeByNames,
44
46
  useQuery: () => useQuery$4,
45
47
  useRemove: () => useRemove$3,
46
- useSetKind: () => useSetKind,
47
48
  useToggleEnable: () => useToggleEnable,
48
49
  useUpsert: () => useUpsert$4
49
50
  });
@@ -115,29 +116,6 @@ const useToggleEnable = defineMutation(() => {
115
116
  key
116
117
  };
117
118
  });
118
- const useSetKind = defineMutation(() => {
119
- const queryCache = useQueryCache();
120
- const key = ["db", "db:plugin:"];
121
- const { mutateAsync, ...mutation } = useMutation({
122
- mutation: async ({ pluginName, kind }) => withTransition(async (trx) => {
123
- const plugin = await trx.selectFrom("plugin").select(["loaderName", "meta"]).where("pluginName", "=", pluginName).executeTakeFirstOrThrow();
124
- if (plugin.loaderName === "builtin") throw new Error("built-in plugin kind cannot be changed");
125
- await trx.updateTable("plugin").set({ meta: JSON.stringify({
126
- ...plugin.meta,
127
- kind
128
- }) }).where("pluginName", "=", pluginName).execute();
129
- }),
130
- onSettled: () => {
131
- queryCache.invalidateQueries({ key });
132
- },
133
- key
134
- });
135
- return {
136
- ...mutation,
137
- setKind: mutateAsync,
138
- key
139
- };
140
- });
141
119
  const useQuery$4 = (query, otherKeys = [], initialData) => useQuery({
142
120
  query: async () => {
143
121
  const { db } = await import("./index.mjs");
@@ -146,8 +124,9 @@ const useQuery$4 = (query, otherKeys = [], initialData) => useQuery({
146
124
  key: () => [
147
125
  "db",
148
126
  "db:plugin:",
149
- query
150
- ].concat(otherKeys),
127
+ query,
128
+ ...otherKeys
129
+ ],
151
130
  staleTime: 15e3,
152
131
  refetchOnMount: "always",
153
132
  initialData,
@@ -283,8 +262,9 @@ const useQueryItem = (query, otherKeys = [], initialData) => useQuery({
283
262
  key: () => [
284
263
  "db:favouriteItem:",
285
264
  "db:favouriteCard:",
286
- query
287
- ].concat(otherKeys),
265
+ query,
266
+ ...toValue(otherKeys)
267
+ ],
288
268
  staleTime: 15e3,
289
269
  initialData,
290
270
  initialDataUpdatedAt: 0
@@ -294,7 +274,11 @@ const useQueryCard = (query, otherKeys = [], initialData) => useQuery({
294
274
  const { db } = await import("./index.mjs");
295
275
  return await query(db.selectFrom("favouriteCard"));
296
276
  },
297
- key: () => ["db:favouriteCard:", query].concat(otherKeys),
277
+ key: () => [
278
+ "db:favouriteCard:",
279
+ query,
280
+ ...toValue(otherKeys)
281
+ ],
298
282
  staleTime: 15e3,
299
283
  refetchOnMount: "always",
300
284
  initialData,
@@ -329,7 +313,7 @@ const useUpsert$2 = defineMutation(() => {
329
313
  await trx.replaceInto("history").values({
330
314
  itemKey,
331
315
  timestamp: Date.now(),
332
- ep: Struct.toRaw(item)
316
+ ep: Struct.toRaw(item.thisEp)
333
317
  }).execute();
334
318
  }, trx),
335
319
  onSettled: () => {
@@ -369,8 +353,9 @@ const useQuery$3 = (query, otherKeys = [], initialData) => useQuery({
369
353
  key: () => [
370
354
  "db",
371
355
  "db:history:",
372
- query
373
- ].concat(otherKeys),
356
+ query,
357
+ ...otherKeys
358
+ ],
374
359
  staleTime: 15e3,
375
360
  refetchOnMount: "always",
376
361
  initialData,
@@ -437,8 +422,9 @@ const useQuery$2 = (query, otherKeys = [], initialData) => useQuery({
437
422
  key: () => [
438
423
  "db",
439
424
  "db:subscribe:",
440
- query
441
- ].concat(otherKeys),
425
+ query,
426
+ ...otherKeys
427
+ ],
442
428
  staleTime: 15e3,
443
429
  refetchOnMount: "always",
444
430
  initialData,
@@ -513,8 +499,9 @@ const useQuery$1 = (query, otherKeys = [], initialData) => useQuery({
513
499
  key: () => [
514
500
  "db",
515
501
  "db:recentView:",
516
- query
517
- ].concat(otherKeys),
502
+ query,
503
+ ...otherKeys
504
+ ],
518
505
  staleTime: 15e3,
519
506
  refetchOnMount: "always",
520
507
  initialData,
@@ -588,6 +575,302 @@ const useConfig = (belongTo, form) => {
588
575
  });
589
576
  return Object.assign(data, { ready });
590
577
  };
578
+ const clientCamelRowSchemas = {
579
+ itemStore: {
580
+ type: "object",
581
+ properties: {
582
+ key: { type: "string" },
583
+ item: { anyOf: [{ type: "object" }, { type: "string" }] }
584
+ },
585
+ required: ["key", "item"],
586
+ additionalProperties: false
587
+ },
588
+ favouriteCard: {
589
+ type: "object",
590
+ properties: {
591
+ createAt: { type: "integer" },
592
+ title: { type: "string" },
593
+ private: { anyOf: [
594
+ { type: "boolean" },
595
+ { const: 0 },
596
+ { const: 1 }
597
+ ] },
598
+ description: { type: "string" }
599
+ },
600
+ required: [
601
+ "createAt",
602
+ "title",
603
+ "private",
604
+ "description"
605
+ ],
606
+ additionalProperties: false
607
+ },
608
+ favouriteItem: {
609
+ type: "object",
610
+ properties: {
611
+ addTime: { type: "integer" },
612
+ belongTo: { type: "integer" },
613
+ itemKey: { type: "string" }
614
+ },
615
+ required: [
616
+ "addTime",
617
+ "belongTo",
618
+ "itemKey"
619
+ ],
620
+ additionalProperties: false
621
+ },
622
+ history: {
623
+ type: "object",
624
+ properties: {
625
+ ep: { anyOf: [{ type: "object" }, { type: "string" }] },
626
+ timestamp: { type: "integer" },
627
+ itemKey: { type: "string" }
628
+ },
629
+ required: [
630
+ "ep",
631
+ "timestamp",
632
+ "itemKey"
633
+ ],
634
+ additionalProperties: false
635
+ },
636
+ recentView: {
637
+ type: "object",
638
+ properties: {
639
+ timestamp: { type: "integer" },
640
+ itemKey: { type: "string" },
641
+ isViewed: { anyOf: [
642
+ { type: "boolean" },
643
+ { const: 0 },
644
+ { const: 1 }
645
+ ] }
646
+ },
647
+ required: [
648
+ "timestamp",
649
+ "itemKey",
650
+ "isViewed"
651
+ ],
652
+ additionalProperties: false
653
+ },
654
+ subscribe: {
655
+ type: "object",
656
+ properties: {
657
+ itemKey: { anyOf: [{ type: "string" }, { type: "null" }] },
658
+ author: { anyOf: [
659
+ { type: "object" },
660
+ { type: "string" },
661
+ { type: "null" }
662
+ ] },
663
+ type: { type: "string" },
664
+ key: { type: "string" },
665
+ plugin: { type: "string" }
666
+ },
667
+ required: [
668
+ "itemKey",
669
+ "author",
670
+ "type",
671
+ "key",
672
+ "plugin"
673
+ ],
674
+ additionalProperties: false
675
+ },
676
+ plugin: {
677
+ type: "object",
678
+ properties: {
679
+ installerName: { type: "string" },
680
+ loaderName: { type: "string" },
681
+ pluginName: { type: "string" },
682
+ meta: { anyOf: [{ type: "object" }, { type: "string" }] },
683
+ enable: { anyOf: [
684
+ { type: "boolean" },
685
+ { const: 0 },
686
+ { const: 1 }
687
+ ] },
688
+ installInput: { type: "string" },
689
+ displayName: { anyOf: [{ type: "string" }, { type: "null" }] }
690
+ },
691
+ required: [
692
+ "installerName",
693
+ "loaderName",
694
+ "pluginName",
695
+ "meta",
696
+ "enable",
697
+ "installInput",
698
+ "displayName"
699
+ ],
700
+ additionalProperties: false
701
+ },
702
+ nativeStore: {
703
+ type: "object",
704
+ properties: {
705
+ namespace: { type: "string" },
706
+ key: { type: "string" },
707
+ value: { type: "string" }
708
+ },
709
+ required: [
710
+ "namespace",
711
+ "key",
712
+ "value"
713
+ ],
714
+ additionalProperties: false
715
+ },
716
+ config: {
717
+ type: "object",
718
+ properties: {
719
+ belongTo: { type: "string" },
720
+ form: { type: "string" },
721
+ data: { type: "string" }
722
+ },
723
+ required: [
724
+ "belongTo",
725
+ "form",
726
+ "data"
727
+ ],
728
+ additionalProperties: false
729
+ }
730
+ };
731
+ //#endregion
732
+ //#region lib/validation.ts
733
+ const validationLogger = logger.scoped("db:validation");
734
+ const validators = /* @__PURE__ */ new WeakMap();
735
+ const validatorFor = (schema) => {
736
+ const cached = validators.get(schema);
737
+ if (cached) return cached;
738
+ const validator = Compile(schema);
739
+ validators.set(schema, validator);
740
+ return validator;
741
+ };
742
+ const validationDetails = (schema, value) => [...Value.Errors(schema, value)].slice(0, 5).map((error) => ({
743
+ instancePath: error.instancePath,
744
+ message: error.message
745
+ }));
746
+ const tableSchema = (table) => clientCamelRowSchemas[table];
747
+ /** Validates a row before writing; throws to block invalid data from persisting. */
748
+ const assertWrite = (schema, table, value) => {
749
+ if (!validatorFor(schema).Check(value)) {
750
+ validationLogger.error("database write validation failed", {
751
+ table,
752
+ errors: validationDetails(schema, value)
753
+ });
754
+ throw new Error(`database write validation failed for ${table}: ${JSON.stringify(validationDetails(schema, value))}`);
755
+ }
756
+ return value;
757
+ };
758
+ const assertWriteRow = (table, value) => assertWrite(tableSchema(table), table, value);
759
+ /** Validates a row on read; failures are non-intrusive: logged and the value returned as-is. */
760
+ const validateRead = (schema, table, value) => {
761
+ if (!validatorFor(schema).Check(value)) validationLogger.warn("database read validation failed", {
762
+ table,
763
+ errors: validationDetails(schema, value)
764
+ });
765
+ return value;
766
+ };
767
+ const validateReadRow = (table, value) => validateRead(tableSchema(table), table, value);
768
+ /** Filters invalid rows on read; failures are non-intrusive: logged and the row dropped. */
769
+ const filterValidRows = (schema, table, rows) => {
770
+ const valid = [];
771
+ for (const row of rows) {
772
+ if (validatorFor(schema).Check(row)) {
773
+ valid.push(row);
774
+ continue;
775
+ }
776
+ validationLogger.warn("database read validation failed, row dropped", {
777
+ table,
778
+ errors: validationDetails(schema, row)
779
+ });
780
+ }
781
+ return valid;
782
+ };
783
+ const filterValidRowsFor = (table, rows) => filterValidRows(tableSchema(table), table, rows);
784
+ //#endregion
785
+ //#region lib/writeValidation.ts
786
+ const isTableNode = (node) => node?.kind === "TableNode";
787
+ const isValuesNode = (node) => node?.kind === "ValuesNode";
788
+ const isValueNode = (node) => node.kind === "ValueNode";
789
+ const isColumnNode = (node) => node.kind === "ColumnNode";
790
+ const tableNameOf = (node) => {
791
+ if (!isTableNode(node)) return null;
792
+ return node.table.identifier.name;
793
+ };
794
+ const rowObjectsOf = (node) => {
795
+ const columns = node.columns;
796
+ if (!columns || columns.length === 0) return null;
797
+ const names = columns.map((column) => column.column.name);
798
+ const values = node.values;
799
+ if (!isValuesNode(values)) return null;
800
+ const rows = [];
801
+ for (const list of values.values) {
802
+ let entries;
803
+ if (list.kind === "PrimitiveValueListNode") entries = [...list.values];
804
+ else if (list.kind === "ValueListNode") {
805
+ const wrapped = [];
806
+ for (const entry of list.values) {
807
+ if (!isValueNode(entry)) return null;
808
+ wrapped.push(entry.value);
809
+ }
810
+ entries = wrapped;
811
+ } else return null;
812
+ if (entries.length !== names.length) return null;
813
+ rows.push(Object.fromEntries(names.map((name, index) => [name, entries[index]])));
814
+ }
815
+ return rows;
816
+ };
817
+ const updatePatchOf = (node) => {
818
+ if (!node.updates || node.updates.length === 0) return null;
819
+ const patch = {};
820
+ for (const update of node.updates) {
821
+ if (!isColumnNode(update.column)) return null;
822
+ const value = update.value;
823
+ if (!isValueNode(value)) return null;
824
+ patch[update.column.column.name] = value.value;
825
+ }
826
+ return patch;
827
+ };
828
+ const partialSchemas = /* @__PURE__ */ new WeakMap();
829
+ const partialOf = (schema) => {
830
+ const cached = partialSchemas.get(schema);
831
+ if (cached) return cached;
832
+ const partial = {
833
+ ...schema,
834
+ required: void 0
835
+ };
836
+ partialSchemas.set(schema, partial);
837
+ return partial;
838
+ };
839
+ const validateWriteNode = (node) => {
840
+ let tableName = null;
841
+ let rows = null;
842
+ let patch = null;
843
+ if (node.kind === "InsertQueryNode") {
844
+ tableName = tableNameOf(node.into);
845
+ rows = rowObjectsOf(node);
846
+ } else if (node.kind === "UpdateQueryNode") {
847
+ tableName = tableNameOf(node.table);
848
+ patch = updatePatchOf(node);
849
+ } else return;
850
+ if (tableName === null) return;
851
+ const schema = clientCamelRowSchemas[tableName];
852
+ if (!schema) return;
853
+ if (rows !== null) {
854
+ if (rows.length === 0) return;
855
+ for (const row of rows) assertWrite(schema, tableName, row);
856
+ return;
857
+ }
858
+ if (patch !== null) assertWrite(partialOf(schema), tableName, patch);
859
+ };
860
+ /**
861
+ * Centralized write gate: validates every insert/replace/update against the
862
+ * generated camelCase row schema before it reaches the dialect. Registered
863
+ * before CamelCasePlugin, so it observes camelCase identifiers and raw values.
864
+ */
865
+ var WriteValidationPlugin = class {
866
+ transformQuery({ node }) {
867
+ validateWriteNode(node);
868
+ return node;
869
+ }
870
+ async transformResult({ result }) {
871
+ return result;
872
+ }
873
+ };
591
874
  //#endregion
592
875
  //#region lib/nativeStore/index.ts
593
876
  const nativeStoreLogger = logger.scoped("db:native-store");
@@ -697,9 +980,13 @@ const createDialect = async () => {
697
980
  };
698
981
  const db = new Kysely({
699
982
  dialect: await createDialect(),
700
- plugins: [new CamelCasePlugin(), new SerializePlugin()]
983
+ plugins: [
984
+ new WriteValidationPlugin(),
985
+ new CamelCasePlugin(),
986
+ new SerializePlugin()
987
+ ]
701
988
  });
702
989
  //#endregion
703
- export { config_exports as ConfigDB, utils_exports as DBUtils, favourite_exports as FavouriteDB, history_exports as HistoryDB, itemStore_exports as ItemStoreDB, plugin_exports as PluginArchiveDB, recentView_exports as RecentDB, subscribe_exports as SubscribeDB, db, isTauriRuntime, useConfig, useNativeStore };
990
+ export { config_exports as ConfigDB, utils_exports as DBUtils, favourite_exports as FavouriteDB, history_exports as HistoryDB, itemStore_exports as ItemStoreDB, plugin_exports as PluginArchiveDB, recentView_exports as RecentDB, subscribe_exports as SubscribeDB, WriteValidationPlugin, assertWrite, assertWriteRow, db, filterValidRows, filterValidRowsFor, isTauriRuntime, tableSchema, useConfig, useNativeStore, validateRead, validateReadRow };
704
991
 
705
992
  //# sourceMappingURL=index.mjs.map