wirejs-resources 0.1.28 → 0.1.30-table-resource

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.d.ts CHANGED
@@ -6,5 +6,6 @@ export { withContext, requiresContext, Context, ContextWrapped } from './adapter
6
6
  export { Resource } from './resource.js';
7
7
  export { overrides } from './overrides.js';
8
8
  export { Secret } from './resources/secret.js';
9
+ export { DistributedTable, DistributedTableOptions, Filter, FieldComparison, FieldComparisonOptions, FieldType, Parser, PassThruParser, RecordType, RecordKey, matchesFilter, } from './resources/distributed-table.js';
9
10
  export * from './types.js';
10
11
  export * from './derived-types.js';
package/dist/index.js CHANGED
@@ -6,5 +6,6 @@ export { withContext, requiresContext, Context } from './adapters/context.js';
6
6
  export { Resource } from './resource.js';
7
7
  export { overrides } from './overrides.js';
8
8
  export { Secret } from './resources/secret.js';
9
+ export { DistributedTable, PassThruParser, matchesFilter, } from './resources/distributed-table.js';
9
10
  export * from './types.js';
10
11
  export * from './derived-types.js';
@@ -5,7 +5,8 @@ import type { Secret } from "./resources/secret";
5
5
  * Used by hosting providers to provide service overrides.
6
6
  */
7
7
  export declare const overrides: {
8
- FileService?: typeof FileService;
9
8
  AuthenticationService?: typeof AuthenticationService;
9
+ DistributedTable?: typeof AuthenticationService;
10
+ FileService?: typeof FileService;
10
11
  Secret?: typeof Secret;
11
12
  };
@@ -0,0 +1,81 @@
1
+ import { Resource } from '../resource.js';
2
+ export type DistributedTableOptions<T> = {
3
+ parse: Parser<T>;
4
+ key: {
5
+ partition: keyof T & string;
6
+ sort?: (keyof T & string)[];
7
+ };
8
+ };
9
+ export type Parser<T> = (record: Record<string, any>) => T;
10
+ export type RecordType<T extends DistributedTableOptions<any>> = T extends DistributedTableOptions<infer IT> ? IT : Record<string, any>;
11
+ export type RecordKey<T, PK extends keyof T, SK extends (keyof T)[] | undefined> = Pick<T, PK | (SK extends string[] ? SK[number] : never)>;
12
+ export type FieldType<T> = undefined extends T ? (T | null) : T;
13
+ export type FieldComparisonOptions<T> = {
14
+ [K in keyof T & string]?: {
15
+ eq: FieldType<T[K]>;
16
+ } | {
17
+ ne: FieldType<T[K]>;
18
+ } | {
19
+ gt: FieldType<T[K]>;
20
+ } | {
21
+ ge: FieldType<T[K]>;
22
+ } | {
23
+ lt: FieldType<T[K]>;
24
+ } | {
25
+ le: FieldType<T[K]>;
26
+ } | {
27
+ between: [FieldType<T[K]>, FieldType<T[K]>];
28
+ } | {
29
+ beginsWith: FieldType<T[K]>;
30
+ };
31
+ };
32
+ export type FieldComparison<T> = {
33
+ [K in keyof FieldComparisonOptions<T>]: Record<K, FieldComparisonOptions<T>[K]> & NoneOf<Exclude<keyof FieldComparisonOptions<T>, K>>;
34
+ }[keyof FieldComparisonOptions<T>];
35
+ export type NoneOf<Fields> = {
36
+ [K in Fields & string]?: never;
37
+ };
38
+ export type Filter<T> = (FieldComparison<T> & NoneOf<'and' | 'or' | 'not'>) | ({
39
+ and: (Filter<T>)[];
40
+ } & NoneOf<keyof T | 'or' | 'not'>) | ({
41
+ or: (Filter<T>)[];
42
+ } & NoneOf<keyof T | 'and' | 'not'>) | ({
43
+ not: Filter<T>;
44
+ } & NoneOf<keyof T | 'and' | 'or'>);
45
+ export declare function matchesFilter<T>(record: T, filter: Filter<T>): boolean;
46
+ export declare function PassThruParser<T>(record: Record<string, any>): T;
47
+ /**
48
+ * A table of records that favors very high *overall* scalability at the expense of
49
+ * scalability *between* partitions. Providers will distribute your data across many
50
+ * servers based on the partition key as the table and/or traffic increases.
51
+ *
52
+ * ### Do NOT change partition keys. (In Production.)
53
+ *
54
+ * Changing it will cause some providers to drop and recreate your table.
55
+ *
56
+ * High cardinality, non-sequential partition keys allow for the best overall scaling.
57
+ */
58
+ export declare class DistributedTable<const P extends Parser<any>, const T extends ReturnType<P>, const PK extends keyof T & string, const SK extends (keyof T & string)[] | undefined> extends Resource {
59
+ #private;
60
+ parse: P;
61
+ partitionKey: PK;
62
+ sort: SK | undefined;
63
+ constructor(scope: Resource | string, id: string, options: {
64
+ parse: P;
65
+ key: {
66
+ partition: PK;
67
+ sort?: SK;
68
+ };
69
+ });
70
+ save(item: T): Promise<void>;
71
+ saveMany(items: T[]): Promise<void>;
72
+ delete(item: RecordKey<T, PK, SK>): Promise<void>;
73
+ deleteMany(items: (RecordKey<T, PK, SK>)[]): Promise<void>;
74
+ get(key: RecordKey<T, PK, SK>): Promise<T | undefined>;
75
+ scan(options: {
76
+ filter?: Filter<T>;
77
+ }): AsyncGenerator<T>;
78
+ query(partition: Pick<T, PK>, options?: {
79
+ filter?: Filter<T>;
80
+ }): AsyncGenerator<T>;
81
+ }
@@ -0,0 +1,130 @@
1
+ import { Resource } from '../resource.js';
2
+ import { FileService } from '../services/file.js';
3
+ import { overrides } from '../overrides.js';
4
+ export function matchesFilter(record, filter) {
5
+ if (filter.and) {
6
+ return filter.and.every(sub => matchesFilter(record, sub));
7
+ }
8
+ if (filter.or) {
9
+ return filter.or.some(sub => matchesFilter(record, sub));
10
+ }
11
+ if (filter.not) {
12
+ return !matchesFilter(record, filter.not);
13
+ }
14
+ for (const [field, fieldFilter] of Object.entries(filter)) {
15
+ const value = record[field];
16
+ if ('eq' in fieldFilter && value !== fieldFilter.eq)
17
+ return false;
18
+ if ('ne' in fieldFilter && value === fieldFilter.ne)
19
+ return false;
20
+ if ('gt' in fieldFilter && value <= fieldFilter.gt)
21
+ return false;
22
+ if ('ge' in fieldFilter && value < fieldFilter.ge)
23
+ return false;
24
+ if ('lt' in fieldFilter && value >= fieldFilter.lt)
25
+ return false;
26
+ if ('le' in fieldFilter && value > fieldFilter.le)
27
+ return false;
28
+ if ('between' in fieldFilter) {
29
+ const [low, high] = fieldFilter.between;
30
+ if (value < low || value > high)
31
+ return false;
32
+ }
33
+ if ('beginsWith' in fieldFilter) {
34
+ if (typeof value !== 'string' || !value.startsWith(fieldFilter.beginsWith)) {
35
+ return false;
36
+ }
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+ export function PassThruParser(record) {
42
+ return record;
43
+ }
44
+ /**
45
+ * A table of records that favors very high *overall* scalability at the expense of
46
+ * scalability *between* partitions. Providers will distribute your data across many
47
+ * servers based on the partition key as the table and/or traffic increases.
48
+ *
49
+ * ### Do NOT change partition keys. (In Production.)
50
+ *
51
+ * Changing it will cause some providers to drop and recreate your table.
52
+ *
53
+ * High cardinality, non-sequential partition keys allow for the best overall scaling.
54
+ */
55
+ export class DistributedTable extends Resource {
56
+ parse;
57
+ partitionKey;
58
+ sort;
59
+ #fileService;
60
+ constructor(scope, id, options) {
61
+ super(scope, id);
62
+ this.parse = options.parse;
63
+ this.partitionKey = options.key.partition;
64
+ this.sort = options.key.sort;
65
+ this.#fileService = new (overrides.FileService || FileService)(this, 'files');
66
+ }
67
+ #getFilename(key) {
68
+ const parts = [key[this.partitionKey]];
69
+ if (this.sort) {
70
+ for (const sk of this.sort) {
71
+ parts.push(key[sk]);
72
+ }
73
+ }
74
+ return parts.map(String).join('__') + '.json';
75
+ }
76
+ async save(item) {
77
+ const key = this.#getFilename(item);
78
+ await this.#fileService.write(key, JSON.stringify(item));
79
+ }
80
+ async saveMany(items) {
81
+ const promises = items.map(item => {
82
+ const key = this.#getFilename(item);
83
+ return this.#fileService.write(key, JSON.stringify(item));
84
+ });
85
+ await Promise.all(promises);
86
+ }
87
+ async delete(item) {
88
+ const key = this.#getFilename(item);
89
+ await this.#fileService.delete(key);
90
+ }
91
+ async deleteMany(items) {
92
+ const promises = items.map(item => {
93
+ const key = this.#getFilename(item);
94
+ return this.#fileService.delete(key);
95
+ });
96
+ await Promise.all(promises);
97
+ }
98
+ async get(key) {
99
+ try {
100
+ const filename = this.#getFilename(key);
101
+ const data = await this.#fileService.read(filename);
102
+ return this.parse(JSON.parse(data));
103
+ }
104
+ catch (err) {
105
+ if (err.code === 'ENOENT')
106
+ return undefined;
107
+ throw err;
108
+ }
109
+ }
110
+ async *scan(options) {
111
+ for await (const filename of this.#fileService.list()) {
112
+ const data = await this.#fileService.read(filename);
113
+ const record = this.parse(JSON.parse(data));
114
+ if (!options.filter || matchesFilter(record, options.filter)) {
115
+ yield record;
116
+ }
117
+ }
118
+ }
119
+ async *query(partition, options = {}) {
120
+ const prefix = partition[this.partitionKey];
121
+ for await (const filename of this.#fileService.list({ prefix })) {
122
+ const data = await this.#fileService.read(filename);
123
+ const record = this.parse(JSON.parse(data));
124
+ if (record[this.partitionKey] === partition[this.partitionKey]
125
+ && (!options.filter || matchesFilter(record, options.filter))) {
126
+ yield record;
127
+ }
128
+ }
129
+ }
130
+ }
@@ -9,7 +9,7 @@ export declare class FileService extends Resource {
9
9
  delete(filename: string): Promise<void>;
10
10
  list({ prefix }?: {
11
11
  prefix?: string | undefined;
12
- }): AsyncGenerator<string, void, unknown>;
12
+ }): AsyncGenerator<string>;
13
13
  isAlreadyExistsError(error: {
14
14
  code: any;
15
15
  }): boolean;
@@ -27,11 +27,21 @@ export class FileService extends Resource {
27
27
  }
28
28
  async *list({ prefix = '' } = {}) {
29
29
  const root = this.#fullNameFor('');
30
- const all = await fs.promises.readdir(root, { recursive: true });
31
- for (const name of all) {
32
- if (prefix === undefined
33
- || name.slice(0, root.length).startsWith(prefix)) {
34
- yield name;
30
+ try {
31
+ const all = await fs.promises.readdir(root, { recursive: true });
32
+ for (const name of all) {
33
+ if (prefix === undefined
34
+ || name.slice(0, root.length).startsWith(prefix)) {
35
+ yield name;
36
+ }
37
+ }
38
+ }
39
+ catch (error) {
40
+ if (error.code === 'ENOENT') {
41
+ return;
42
+ }
43
+ else {
44
+ throw error;
35
45
  }
36
46
  }
37
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-resources",
3
- "version": "0.1.28",
3
+ "version": "0.1.30-table-resource",
4
4
  "description": "Basic services and server-side resources for wirejs apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",