Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve memory usage of record export #311

Merged
merged 22 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
86b66b3
refactor: rename KintoneRecord to LocalRecord
tasshi-me Apr 14, 2023
3618bfc
implement getAllRecordsGenerator
tasshi-me Apr 14, 2023
8584374
make Stringifier accepts iterative values
tasshi-me Apr 20, 2023
8602c5e
fix: remove jsonStringifier
tasshi-me Apr 24, 2023
9c9582b
refactor: create abstract CliKintoneError class
tasshi-me Apr 25, 2023
ddf3406
refactor: abstract local destination to Repository
tasshi-me Apr 26, 2023
6d94bb0
fix: apply encoding
tasshi-me Apr 26, 2023
eac81db
fix: reimplement stringifier with stream
tasshi-me Apr 27, 2023
5724193
test: add test of stringifier with no record
tasshi-me Apr 27, 2023
a2f0e39
refactor: remove unused imports
tasshi-me Apr 27, 2023
94644f6
refactor: make some fields of Repository private
tasshi-me Apr 27, 2023
1766365
refactor: rename the method opening source/sink of repository
tasshi-me Apr 27, 2023
d3289a3
refactor: remove duplicated implementation of `CliKintoneError.toStri…
tasshi-me Apr 27, 2023
5cf22b4
chore: apply no-unused-imports rule
tasshi-me Apr 27, 2023
0a03e11
refactor: remove unused RepositoryError of record exporting
tasshi-me Apr 27, 2023
14cea36
Merge branch 'main' into feat/improve-memory-usage-of-export
tasshi-me Apr 27, 2023
6aa1d5b
refactor: remove Record type and use KintoneRecordForResponse
tasshi-me Apr 28, 2023
46487af
refactor: remove unused vars
tasshi-me Apr 28, 2023
b7b8489
chore: use `@typescript-eslint/no-unused-vars` rule to remove unused …
tasshi-me May 8, 2023
a181095
refactor: define our Writer and Stringifier interface
tasshi-me May 8, 2023
2d22957
refactor: rename RecordTransform to RecordsTransform
tasshi-me May 9, 2023
22f2d02
Merge remote-tracking branch 'origin/main' into feat/improve-memory-u…
tasshi-me May 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ const config = {
prefer: "type-imports",
},
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
],
},
};
module.exports = config;
23 changes: 6 additions & 17 deletions src/record/delete/parsers/error.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { CsvError } from "csv-parse";
import { CliKintoneError } from "../../../utils/error";

export class ParserError extends Error {
private readonly cause: unknown;

export class ParserError extends CliKintoneError {
constructor(cause: unknown) {
const message = "Failed to parse input";
super(message);

super(message, cause);
this.name = "ParserError";
this.message = message;
this.cause = cause;

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
// Set the prototype explicitly.
Object.setPrototypeOf(this, ParserError.prototype);
}

toString(): string {
let errorMessage = "";
errorMessage += this.message + "\n";
protected _toStringCause(): string {
if (this.cause instanceof CsvError) {
errorMessage += `${this.cause.code}: ${this.cause.message}\n`;
} else {
errorMessage += this.cause + "\n";
return `${this.cause.code}: ${this.cause.message}\n`;
}
return errorMessage;
return super._toStringCause();
}
}
37 changes: 13 additions & 24 deletions src/record/delete/usecases/deleteAll/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,40 @@ import { KintoneAllRecordsError } from "@kintone/rest-api-client";
import type { KintoneRecordForDeleteAllParameter } from "../../../../kintone/types";
import { kintoneAllRecordsErrorToString } from "../../../error";
import { ErrorParser } from "../../utils/error";
import { CliKintoneError } from "../../../../utils/error";

export class DeleteAllRecordsError extends CliKintoneError {
readonly detail: string;

export class DeleteAllRecordsError extends Error {
private readonly cause: unknown;
private readonly records: KintoneRecordForDeleteAllParameter[];
private readonly numOfSuccess: number;
private readonly numOfTotal: number;

constructor(cause: unknown, records: KintoneRecordForDeleteAllParameter[]) {
const message = "Failed to delete all records.";
super(message);
super(message, cause);

this.name = "DeleteAllRecordsError";
this.message = message;
this.cause = cause;
this.records = records;
this.numOfTotal = this.records.length;
this.numOfSuccess = 0;
if (this.cause instanceof KintoneAllRecordsError) {
this.numOfSuccess = this.cause.numOfProcessedRecords;
}

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
// Set the prototype explicitly.
Object.setPrototypeOf(this, DeleteAllRecordsError.prototype);
}

toString(): string {
let errorMessage = "";
errorMessage += this.message + "\n";

if (this.numOfSuccess === 0) {
errorMessage += `No records are deleted.\n`;
this.detail = `No records are deleted.`;
} else {
errorMessage += `${this.numOfSuccess}/${this.numOfTotal} records are deleted successfully.\n`;
this.detail = `${this.numOfSuccess}/${this.numOfTotal} records are deleted successfully.`;
}

Object.setPrototypeOf(this, DeleteAllRecordsError.prototype);
}

protected _toStringCause(): string {
if (this.cause instanceof KintoneAllRecordsError) {
errorMessage += kintoneAllRecordsErrorToString(
new ErrorParser(this.cause)
);
} else if (this.cause instanceof DeleteAllRecordsError) {
errorMessage += this.cause.toString();
} else {
errorMessage += this.cause + "\n";
return kintoneAllRecordsErrorToString(new ErrorParser(this.cause));
}
return errorMessage;
return super._toStringCause();
}
}
37 changes: 13 additions & 24 deletions src/record/delete/usecases/deleteByRecordNumber/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,40 @@ import { KintoneAllRecordsError } from "@kintone/rest-api-client";
import type { KintoneRecordForDeleteAllParameter } from "../../../../kintone/types";
import { kintoneAllRecordsErrorToString } from "../../../error";
import { ErrorParser } from "../../utils/error";
import { CliKintoneError } from "../../../../utils/error";

export class DeleteSpecifiedRecordsError extends CliKintoneError {
readonly detail: string;

export class DeleteSpecifiedRecordsError extends Error {
private readonly cause: unknown;
private readonly records: KintoneRecordForDeleteAllParameter[];
private readonly numOfSuccess: number;
private readonly numOfTotal: number;

constructor(cause: unknown, records: KintoneRecordForDeleteAllParameter[]) {
const message = "Failed to delete records.";
super(message);
super(message, cause);

this.name = "DeleteSpecifiedRecordsError";
this.message = message;
this.cause = cause;
this.records = records;
this.numOfTotal = this.records.length;
this.numOfSuccess = 0;
if (this.cause instanceof KintoneAllRecordsError) {
this.numOfSuccess = this.cause.numOfProcessedRecords;
}

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
// Set the prototype explicitly.
Object.setPrototypeOf(this, DeleteSpecifiedRecordsError.prototype);
}

toString(): string {
let errorMessage = "";
errorMessage += this.message + "\n";

if (this.numOfSuccess === 0) {
errorMessage += `No records are deleted.\n`;
this.detail = `No records are deleted.`;
} else {
errorMessage += `${this.numOfSuccess}/${this.numOfTotal} records are deleted successfully.\n`;
this.detail = `${this.numOfSuccess}/${this.numOfTotal} records are deleted successfully.`;
}

Object.setPrototypeOf(this, DeleteSpecifiedRecordsError.prototype);
}

protected _toStringCause(): string {
if (this.cause instanceof KintoneAllRecordsError) {
errorMessage += kintoneAllRecordsErrorToString(
new ErrorParser(this.cause)
);
} else if (this.cause instanceof DeleteSpecifiedRecordsError) {
errorMessage += this.cause.toString();
} else {
errorMessage += this.cause + "\n";
return kintoneAllRecordsErrorToString(new ErrorParser(this.cause));
}
return errorMessage;
return super._toStringCause();
}
}
18 changes: 3 additions & 15 deletions src/record/delete/validator/error.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
export class ValidatorError extends Error {
private readonly cause: unknown;
import { CliKintoneError } from "../../../utils/error";

export class ValidatorError extends CliKintoneError {
constructor(cause: unknown) {
const message = "Failed to delete records";
super(message);
super(message, cause);

this.name = "ValidatorError";
this.message = message;
this.cause = cause;

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
// Set the prototype explicitly.
Object.setPrototypeOf(this, ValidatorError.prototype);
}

toString(): string {
let errorMessage = "";
errorMessage += this.message + "\n";
errorMessage += this.cause + "\n";

return errorMessage;
}
}
2 changes: 1 addition & 1 deletion src/record/delete/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const validateRecordNumbers: (
const errorHandler = new ErrorHandler();
const hasAppCodePrevious = hasAppCode(recordNumbers[0].value, appCode);
const allRecordIds = await getAllRecordIds(apiClient, app);
recordNumbers.forEach((recordNumber, index) => {
recordNumbers.forEach((recordNumber) => {
const value = recordNumber.value;
if (value.length === 0 || !isValidRecordNumber(value, appCode)) {
errorHandler.addInvalidValueError(recordNumber);
Expand Down
23 changes: 14 additions & 9 deletions src/record/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import iconv from "iconv-lite";
import type { RestAPIClientOptions } from "../../kintone/client";
import { buildRestAPIClient } from "../../kintone/client";
import { getRecords } from "./usecases/get";
import { stringifierFactory } from "./stringifiers";
import { createSchema } from "./schema";
import { formLayout as defaultTransformer } from "./schema/transformers/formLayout";
import { userSelected } from "./schema/transformers/userSelected";
import { logger } from "../../utils/log";
import { LocalRecordRepositoryFromStream } from "./repositories/localRecordRepositoryFromStream";
import { Transform } from "stream";

export type ExportFileEncoding = "utf8" | "sjis";

Expand Down Expand Up @@ -42,18 +43,22 @@ export const run: (
? userSelected(fields, fieldsJson, layoutJson)
: defaultTransformer(layoutJson)
);
const records = await getRecords(apiClient, app, schema, {

const repository = new LocalRecordRepositoryFromStream(
() => {
const encodeStream = Transform.from(iconv.encodeStream(encoding));
encodeStream.pipe(process.stdout);
return encodeStream;
},
schema,
!!attachmentsDir
);

await getRecords(apiClient, app, repository, schema, {
condition,
orderBy,
attachmentsDir,
});
const stringifier = stringifierFactory({
format: "csv",
schema,
useLocalFilePath: !!attachmentsDir,
});
const stringifiedRecords = stringifier(records);
process.stdout.write(iconv.encode(stringifiedRecords, encoding));
} catch (e) {
logger.error(e);
// eslint-disable-next-line no-process-exit
Expand Down
30 changes: 30 additions & 0 deletions src/record/export/repositories/localRecordRepositoryFromStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { LocalRecordRepository } from "../usecases/interface";
import type { RecordSchema } from "../types/schema";
import { stringifierFactory } from "./stringifiers";

export class LocalRecordRepositoryFromStream implements LocalRecordRepository {
readonly format = "csv";

private readonly openWritableSink: () => NodeJS.WritableStream;
private readonly schema: RecordSchema;
private readonly useLocalFilePath: boolean;

constructor(
openWritableSink: () => NodeJS.WritableStream,
schema: RecordSchema,
useLocalFilePath: boolean
) {
this.openWritableSink = openWritableSink;
this.schema = schema;
this.useLocalFilePath = useLocalFilePath;
}
writer() {
const stringifier = stringifierFactory({
format: this.format,
schema: this.schema,
useLocalFilePath: this.useLocalFilePath,
});
stringifier.pipe(this.openWritableSink());
return stringifier;
}
}
26 changes: 26 additions & 0 deletions src/record/export/repositories/localRecordRepositoryMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { LocalRecordRepository, Writer } from "../usecases/interface";
import type { LocalRecord } from "../types/record";

export class LocalRecordRepositoryMock implements LocalRecordRepository {
readonly format = "csv";

private underlyingSink: WritableMock = new WritableMock();

writer() {
this.underlyingSink = new WritableMock();
return this.underlyingSink;
}
receivedRecords() {
return this.underlyingSink.underlyingSink;
}
}

class WritableMock implements Writer {
public underlyingSink: LocalRecord[] = [];
async write(chunk: LocalRecord[]) {
this.underlyingSink.push(...chunk);
}
async end() {
/* noop */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { input } from "./input";
import { expected } from "./expected_csv";
import { schema } from "./schema";

export const pattern = {
input: input,
expected: expected,
schema: schema,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { KintoneRecord } from "../../../types/record";
import type { LocalRecord } from "../../../../../types/record";

export const input: KintoneRecord[] = [
export const input: LocalRecord[] = [
{
レコード番号: {
type: "RECORD_NUMBER",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RecordSchema } from "../../../types/schema";
import type { RecordSchema } from "../../../../../types/schema";

export const schema: RecordSchema = {
hasSubtable: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const expected = `"レコード番号","文字列__1行_","作成日時"
"1","文字列123","2022-05-02T07:58:00Z"
"2","文字列234","2022-05-02T07:58:00Z"
"3","文字列345","2022-05-02T07:58:00Z"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { input } from "./input";
import { expected } from "./expected_csv";
import { schema } from "./schema";

export const pattern = {
input: input,
expected: expected,
schema: schema,
};
Loading